## 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)
6.5 KiB
Vendored
Generated
bufq
This is an internal module for managing I/O buffers. A bufq can be written
to and read from. It manages read and write positions and has a maximum size.
read/write
Its basic read/write functions have a similar signature and return code handling as many internal curl read and write ones.
CURLcode Curl_bufq_write(struct bufq *q,
const uint8_t *buf, size_t len,
size_t *pnwritten);
- sets
pnwrittento the length written intoqor -1 on error. - writing to a full
qsetspnwrittento -1 and returns CURLE_AGAIN
CURLcode Curl_bufq_read(struct bufq *q, uint8_t *buf, size_t len,
size_t *pnread);
- sets
pnreadto the length read fromqor -1 on error. - reading from an empty
qsetspnreadto -1 and returns CURLE_AGAIN
To pass data into a bufq without an extra copy, read callbacks can be used.
typedef CURLcode Curl_bufq_reader(void *reader_ctx,
uint8_t *buf, size_t len,
size_t *pnread);
CURLcode Curl_bufq_slurp(struct bufq *q, Curl_bufq_reader *reader,
void *reader_ctx, size_t *pnread);
Curl_bufq_slurp() invokes the given reader callback, passing it its own
internal buffer memory to write to. It may invoke the reader several times,
as long as it has space and while the reader always returns the length that
was requested. There are variations of slurp that call the reader at most
once or only read in a maximum amount of bytes.
The analog mechanism for write out buffer data is:
typedef CURLcode Curl_bufq_writer(void *writer_ctx,
const uint8_t *buf, size_t len,
size_t *pwritten);
CURLcode Curl_bufq_pass(struct bufq *q, Curl_bufq_writer *writer,
void *writer_ctx, size_t *pwritten);
Curl_bufq_pass() invokes the writer, passing its internal memory and
remove the amount that writer reports.
peek and skip
It is possible to get access to the memory of data stored in a bufq with:
bool Curl_bufq_peek(struct bufq *q,
const uint8_t **pbuf, size_t *plen);
On returning TRUE, pbuf points to internal memory with plen bytes that one
may read. This is only valid until another operation on bufq is performed.
Instead of reading bufq data, one may skip it:
void Curl_bufq_skip(struct bufq *q, size_t amount);
This removes amount number of bytes from the bufq.
lifetime
bufq is initialized and freed similar to the dynbuf module. Code using
bufq holds a struct bufq somewhere. Before it uses it, it invokes:
void Curl_bufq_init(struct bufq *q, size_t chunk_size, size_t max_chunks);
The bufq is told how many "chunks" of data it shall hold at maximum and how
large those "chunks" should be. There are some variants of this, allowing for
more options. How "chunks" are handled in a bufq is presented in the section
about memory management.
The user of the bufq has the responsibility to call:
void Curl_bufq_free(struct bufq *q);
to free all resources held by q. It is possible to reset a bufq to empty via:
void Curl_bufq_reset(struct bufq *q);
memory management
Internally, a bufq uses allocation of fixed size, e.g. the "chunk_size", up
to a maximum number, e.g. "max_chunks". These chunks are allocated on demand,
therefore writing to a bufq may return CURLE_OUT_OF_MEMORY. Once the max
number of chunks are used, the bufq reports that it is "full".
Each chunks has a read and write index. A bufq keeps its chunks in a
list. Reading happens always at the head chunk, writing always goes to the
tail chunk. When the head chunk becomes empty, it is removed. When the tail
chunk becomes full, another chunk is added to the end of the list, becoming
the new tail.
Chunks that are no longer used are returned to a spare list by default. If
the bufq is created with option BUFQ_OPT_NO_SPARES those chunks are freed
right away.
If a bufq is created with a bufc_pool, the no longer used chunks are
returned to the pool. Also bufq asks the pool for a chunk when it needs one.
More in section "pools".
empty, full and overflow
One can ask about the state of a bufq with methods such as
Curl_bufq_is_empty(q), Curl_bufq_is_full(q), etc. The amount of data held
by a bufq is the sum of the data in all its chunks. This is what is reported
by Curl_bufq_len(q).
Note that a bufq length and it being "full" are only loosely related. A
simple example:
- create a
bufqwith chunk_size=1000 and max_chunks=4. - write 4000 bytes to it, it reports "full"
- read 1 bytes from it, it still reports "full"
- read 999 more bytes from it, and it is no longer "full"
The reason for this is that full really means: bufq uses max_chunks and the last one cannot be written to.
When you read 1 byte from the head chunk in the example above, the head still hold 999 unread bytes. Only when those are also read, can the head chunk be removed and a new tail be added.
There is another variation to this. If you initialized a bufq with option
BUFQ_OPT_SOFT_LIMIT, it allows writes beyond the max_chunks. It
reports full, but one can still write. This option is necessary, if
partial writes need to be avoided. It means that you need other checks to keep
the bufq from growing ever larger and larger.
pools
A struct bufc_pool may be used to create chunks for a bufq and keep spare
ones around. It is initialized and used via:
void Curl_bufcp_init(struct bufc_pool *pool,
size_t chunk_size, size_t spare_max);
void Curl_bufq_initp(struct bufq *q, struct bufc_pool *pool,
size_t max_chunks, int opts);
The pool gets the size and the mount of spares to keep. The bufq gets the
pool and the max_chunks. It no longer needs to know the chunk sizes, as
those are managed by the pool.
A pool can be shared between many bufqs, as long as all of them operate in
the same thread. In curl that would be true for all transfers using the same
multi handle. The advantages of a pool are:
- when all
bufqs are empty, only memory formax_sparechunks in the pool is used. Emptybufqs holds no memory. - the latest spare chunk is the first to be handed out again, no matter which
bufqneeds it. This keeps the footprint of "recently used" memory smaller.