Files
jak-project/third-party/curl/scripts/mdlinkcheck
T
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

255 lines
7.2 KiB
Perl
Vendored
Generated

#!/usr/bin/env perl
#***************************************************************************
# _ _ ____ _
# 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
#
###########################################################################
use strict;
use warnings;
my %whitelist = (
'https://curl.se' => 1,
'https://curl.se/' => 1,
'https://curl.se/bug/' => 1,
'https://curl.se/bug/view.cgi' => 1,
'https://curl.se/changes.html' => 1,
'https://curl.se/dev/advisory.html' => 1,
'https://curl.se/dev/builds.html' => 1,
'https://curl.se/dev/code-style.html' => 1,
'https://curl.se/dev/contribute.html' => 1,
'https://curl.se/dev/internals.html' => 1,
'https://curl.se/dev/secprocess.html' => 1,
'https://curl.se/dev/sourceactivity.html' => 1,
'https://curl.se/docs/' => 1,
'https://curl.se/docs/caextract.html' => 1,
'https://curl.se/docs/copyright.html' => 1,
'https://curl.se/docs/http-cookies.html' => 1,
'https://curl.se/docs/install.html' => 1,
'https://curl.se/docs/knownbugs.html' => 1,
'https://curl.se/docs/manpage.html' => 1,
'https://curl.se/docs/releases.html' => 1,
'https://curl.se/docs/security.html' => 1,
'https://curl.se/docs/ssl-ciphers.html' => 1,
'https://curl.se/docs/ssl-compared.html' => 1,
'https://curl.se/docs/sslcerts.html' => 1,
'https://curl.se/docs/thanks.html' => 1,
'https://curl.se/docs/todo.html' => 1,
'https://curl.se/docs/vulnerabilities.html' => 1,
'https://curl.se/download.html' => 1,
'https://curl.se/libcurl/' => 1,
'https://curl.se/libcurl/c/CURLOPT_SSL_CIPHER_LIST.html' => 1,
'https://curl.se/libcurl/c/CURLOPT_SSLVERSION.html' => 1,
'https://curl.se/libcurl/c/CURLOPT_TLS13_CIPHERS.html' => 1,
'https://curl.se/libcurl/c/libcurl.html' => 1,
'https://curl.se/libcurl/c/threadsafe.html' => 1,
'https://curl.se/logo/curl-logo.svg' => 1,
'https://curl.se/mail/' => 1,
'https://curl.se/mail/etiquette.html' => 1,
'https://curl.se/mail/list.cgi?list=curl-distros' => 1,
'https://curl.se/mail/list.cgi?list=curl-library' => 1,
'https://curl.se/rfc/cookie_spec.html' => 1,
'https://curl.se/rfc/rfc2255.txt' => 1,
'https://curl.se/sponsors.html' => 1,
'https://curl.se/support.html' => 1,
'https://curl.se/windows/' => 1,
'https://testclutch.curl.se/' => 1,
'https://github.com/curl/curl-fuzzer' => 1,
'https://github.com/curl/curl-www' => 1,
'https://github.com/curl/curl/wcurl' => 1,
);
my %url;
my %flink;
my $dry;
if(defined $ARGV[0] && $ARGV[0] eq "--dry-run") {
$dry = 1;
shift @ARGV;
}
# list all files to scan for links
my @files = qx(git ls-files docs include lib scripts src);
sub storelink {
my ($f, $line, $link) = @_;
my $o = $link;
if($link =~ /^\#/) {
# ignore local-only links
return;
}
# cut off any anchor
$link =~ s:\#.*\z::;
if($link =~ /^(https|http):/) {
if($whitelist{$link}) {
#print "-- whitelisted: $link\n";
$whitelist{$link}++;
}
# example.com is used as example
elsif($link =~ /^https:\/\/(.*)example.(com|org|net)/) {
#print "-- example: $link\n";
}
# so is using the .example TLD
elsif($link =~ /^https:\/\/(.*)\.example(\/|$|:)/) {
#print "-- .example: $link\n";
}
# so is using anything on localhost
elsif($link =~ /^http(s|):\/\/localhost/) {
#print "-- localhost: $link\n";
}
# ignore all links to curl's github repo
elsif($link =~ /^https:\/\/github.com\/curl\/curl(\/|$)/) {
#print "-- curl github repo: $link\n";
}
elsif($link =~ /^(https|http):\/\/[0-9.]+(\/|$)/) {
#print "-- IPv4 number: $link\n";
}
else {
#print "ADD '$link'\n";
$url{$link} .= "$f:$line ";
}
return;
}
# a file link
my $dir = $f;
$dir =~ s:([^/]*\z)::;
if($link =~ s/(^\/)//) {
# link starts with a slash, now removed
$dir = "";
}
else {
while($link =~ s:^\.\.\/::) {
$dir =~ s:([^/]*)\/\z::;
}
}
$flink{"./$dir$link"} .= "$f:$line ";
}
sub findlinks {
my ($f) = @_;
my $line = 1;
open(F, "<:crlf", $f) or
return;
# is it a markdown extension?
my $md = ($f =~ /\.md$/i);
while(<F>) {
chomp;
if($md && /\]\(([^)]*)/) {
my $link = $1;
#print "$f:$line $link\n";
storelink($f, $line, $link);
}
# ignore trailing: dot, double quote, single quote, asterisk, hash,
# comma, question mark, colon, closing parenthesis, backslash,
# closing angle bracket, whitespace, pipe, backtick, semicolon
elsif(/(https:\/\/[a-z0-9.\/:%_+@-]+[^."'*\#,?:\)> \t|`;\\])/i) {
#print "RAW '$_'\n";
storelink($f, $line, $1);
}
$line++;
}
close(F);
}
sub checkurl {
my ($url) = @_;
if($whitelist{$url}) {
#print STDERR "$url is whitelisted\n";
return 0;
}
$url =~ s/\+/%2B/g;
my @content;
if(open(my $fh, '-|', 'curl', '-ILfsm10', '--retry', '2', '--retry-delay', '5',
'-A', 'Mozilla/curl.se link-probe', $url)) {
@content = <$fh>;
close $fh;
}
if(!$content[0]) {
print "FAIL: $url\n";
return 1; # fail
}
print "OK: $url\n";
return 0; # ok
}
for my $f (@files) {
chomp $f;
if($f !~ /\/mdlinkcheck$/) {
findlinks($f);
}
}
for my $u (sort keys %whitelist) {
if($whitelist{$u} == 1) {
printf STDERR "warning: unused whitelist entry: '$u'\n";
}
}
if($dry) {
for my $u (sort keys %url) {
print "$u\n";
}
exit;
}
my $error;
my @errlist;
for my $u (sort keys %url) {
my $r = checkurl($u);
if($r) {
for my $f (split(/ /, $url{$u})) {
push @errlist, sprintf "%s ERROR links to missing URL %s\n", $f, $u;
$error++;
}
}
}
for my $l (sort keys %flink) {
if(! -r $l) {
for my $f (split(/ /, $flink{$l})) {
push @errlist, sprintf "%s ERROR links to missing file %s\n", $f, $l;
$error++;
}
}
}
printf "Checked %d URLs\n", scalar(keys %url);
if($error) {
print "$error URLs had problems:\n";
for(@errlist) {
print $_;
}
}
exit 1 if($error);