feat: add Kimi Code and web APIs

This commit is contained in:
WHC
2026-10-19 10:41:00 +08:00
parent 60047ca5ce
commit 235652604d
21 changed files with 1189 additions and 49 deletions
+273
View File
@@ -0,0 +1,273 @@
#!/usr/bin/env bash
# Build and pack the Windows GUI (app/cst-agent-app.exe) from Linux.
#
# scripts/package_windows_app.sh (run it from the repo root)
# nix-shell shell-win.nix --run scripts/package_windows_app.sh
# --run works from anywhere; the script re-enters the shell itself when the
# mingw toolchain is not on PATH.
#
# The toolchain is the same one the TUI cross build uses (shell-win.nix: mingw
# UCRT, x86_64-w64-mingw32). Qt comes from MSYS2, prebuilt for exactly that
# target, and the host (Linux) tools come from nixpkgs' native Qt6: both are
# 6.11.2, which is what Qt's own version check wants.
#
# Why not nixpkgs' mingw Qt: it does not build in this nixpkgs. bash 5.3 fails
# to cross-compile for mingw (`sigset_t` is not in the mingw headers), which
# takes tcl down, which sqlite needs, which qtbase needs; before that, giflib's
# mingw patch no longer applies to its own source. Three separate overlays got
# as far as qtbase before that wall. MSYS2 has the same Qt prebuilt.
#
# MSYS2's Qt ships *Windows* tools (moc.exe, rcc.exe, qmlcachegen.exe, ...);
# the build runs on Linux, so each one is replaced by a one-line shim that
# calls the host tool. windeployqt cannot run here either, so the deployment
# half of this script copies the DLLs, plugins and QML modules by hand: the
# DLLs by walking the exe's imports, the rest from the layout Qt expects.
#
# Output: dist/windows-x86_64-app/ and
# dist/cst-agent-app-VERSION-windows-x86_64.zip (+ .sha256)
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
MIRROR="${CST_MSYS_MIRROR:-https://mirror.msys2.org/mingw/ucrt64}"
PKGS="$ROOT/third_party/msys-qt6" # MSYS2 packages, extracted
PREFIX="$PKGS/ucrt64" # what CMAKE_PREFIX_PATH points at
HOST="${CST_QT6_HOST:-$ROOT/third_party/prefix-qt6-host}"
BUILD="$ROOT/build-win-app"
OUT="$ROOT/dist/windows-x86_64-app"
if ! command -v x86_64-w64-mingw32-gcc >/dev/null && [[ -z "${IN_NIX_SHELL:-}" ]] &&
command -v nix-shell >/dev/null; then
exec nix-shell "$ROOT/shell-win.nix" --run "$(printf '%q ' "$0" "$@")"
fi
command -v x86_64-w64-mingw32-gcc >/dev/null || {
echo "need the mingw UCRT toolchain (run through shell-win.nix)" >&2
exit 1
}
OBJDUMP="$(command -v x86_64-w64-mingw32-objdump || echo objdump)"
STRIP="$(command -v x86_64-w64-mingw32-strip || echo strip)"
# --- the MSYS2 packages ----------------------------------------------------
# The Qt modules the app links, plus every package that provides a DLL those
# modules load (found with x86_64-w64-mingw32-objdump -p, then against
# ucrt64.files from the same mirror). Versions are taken from the mirror
# listing, so a new Qt6 release only means a new run of this script.
MSYS_PKGS=(
mingw-w64-ucrt-x86_64-qt6-base
mingw-w64-ucrt-x86_64-qt6-declarative
mingw-w64-ucrt-x86_64-qt6-svg
mingw-w64-ucrt-x86_64-libgcc
mingw-w64-ucrt-x86_64-libstdc++
mingw-w64-ucrt-x86_64-libwinpthread
mingw-w64-ucrt-x86_64-libb2
mingw-w64-ucrt-x86_64-double-conversion
mingw-w64-ucrt-x86_64-icu
mingw-w64-ucrt-x86_64-pcre2
mingw-w64-ucrt-x86_64-zlib
mingw-w64-ucrt-x86_64-zstd
mingw-w64-ucrt-x86_64-freetype
mingw-w64-ucrt-x86_64-harfbuzz
mingw-w64-ucrt-x86_64-md4c
mingw-w64-ucrt-x86_64-libpng
mingw-w64-ucrt-x86_64-brotli
mingw-w64-ucrt-x86_64-bzip2
# harfbuzz reaches for these (and glib2 for libffi/gettext below): they are
# not what the app links directly, but the shaper the font engine loads.
mingw-w64-ucrt-x86_64-glib2
mingw-w64-ucrt-x86_64-graphite2
mingw-w64-ucrt-x86_64-libffi
mingw-w64-ucrt-x86_64-gettext-runtime
mingw-w64-ucrt-x86_64-libiconv
)
listing="$PKGS/listing.html"
mkdir -p "$PKGS"
if [[ ! -s "$listing" || -n "${CST_MSYS_REFRESH:-}" ]]; then
echo "==> mirror listing"
curl -fsSL "$MIRROR/" -o "$listing"
fi
# The newest file of a package (no -debug, no .sig): the mirror keeps every
# revision, and the listing is one HTTP page, so this is the whole lookup. The
# names come out of the links (the text of a link is elided with "..") and are
# matched literally, because a package name may hold regex characters
# ("libstdc++", which the listing writes as %2B and the link as a plus).
allPkgs="$(grep -oE 'href="[^"]+\.pkg\.tar\.zst"' "$listing" |
sed 's/^href="//; s/"$//; s/%2[bB]/+/g' | grep -v -- '-debug-' | sort -V -u)"
newest() { # newest PKGNAME
# ... and the name has to end there: "harfbuzz-" also prefixes
# "harfbuzz-utils", "brotli-" prefixes "brotli-testdata", "zlib-" prefixes
# "zlib-ng-compat". What follows the name is a version, which starts with a
# digit.
printf '%s\n' "$allPkgs" | awk -v pre="$1-" '
index($0, pre) == 1 && substr($0, length(pre) + 1) ~ /^[0-9]/' |
sort -V | tail -1
}
for p in "${MSYS_PKGS[@]}"; do
f="$(newest "$p")"
[[ -n "$f" ]] || { echo "no package $p in the mirror listing" >&2; exit 1; }
if [[ ! -f "$PKGS/$f" ]]; then
echo "==> $f"
curl -fsSL "$MIRROR/$f" -o "$PKGS/$f"
fi
# A package that is already extracted leaves its files in place: tar is
# happy to overwrite, so this just refreshes whatever changed.
tar --zstd -xf "$PKGS/$f" -C "$PKGS"
done
[[ -f "$PREFIX/lib/cmake/Qt6/Qt6Config.cmake" ]] || {
echo "$PREFIX does not look like a Qt6 install" >&2
exit 1
}
# --- the host tools --------------------------------------------------------
# nixpkgs' native Qt 6, in one prefix: moc, rcc, qmlcachegen, qmltyperegistrar,
# qmlimportscanner, qtpaths and their CMake packages (see scripts/qt6-host.nix).
if [[ ! -x "$HOST/libexec/moc" ]]; then
echo "==> host Qt6 tools"
nix build --impure -f "$ROOT/scripts/qt6-host.nix" --out-link "$HOST"
fi
# Every .exe MSYS2 ships is a Windows program: replace each with a shim that
# runs the host tool of the same name. Done in the extracted tree, so the
# Qt6*Tools CMake packages keep pointing at the paths they record.
shim() { # shim FILE HOSTTOOL
printf '#!/bin/sh\nexec %s "$@"\n' "$2" >"$1"
chmod +x "$1"
}
shimmed=0
for f in "$PREFIX"/share/qt6/bin/*.exe "$PREFIX"/bin/*.exe; do
[[ -e "$f" ]] || continue
n="$(basename "$f" .exe)"
for cand in "$HOST/libexec/$n" "$HOST/bin/$n"; do
if [[ -x "$cand" ]]; then
shim "$f" "$cand"
shimmed=$((shimmed + 1))
break
fi
done
done
echo "==> $shimmed Qt tools point at the host ones"
# --- build -----------------------------------------------------------------
echo "==> cmake (Windows, GUI)"
cmake -S "$ROOT" -B "$BUILD" -G Ninja \
-DCMAKE_TOOLCHAIN_FILE="$ROOT/cmake/mingw-w64-x86_64.cmake" \
-DCMAKE_BUILD_TYPE=Release \
-DCST_BUILD_TUI=OFF -DCST_BUILD_APP=ON \
-DCMAKE_PREFIX_PATH="$PREFIX" \
-DQT_HOST_PATH="$HOST"
# Only the app: the Win32 test targets are host-unrunnable and some of them are
# POSIX (tests/test_tools_ext.c calls mkdir with two arguments), exactly as in
# the TUI cross build.
cmake --build "$BUILD" -j"$(nproc)" --target cst-agent-app
EXE="$BUILD/app/cst-agent-app.exe"
[[ -f "$EXE" ]] || { echo "no cst-agent-app.exe" >&2; exit 1; }
# --- the package -----------------------------------------------------------
ver="$(sed -n 's/^#define CST_VERSION_\(MAJOR\|MINOR\|PATCH\) \([0-9]*\)$/\2/p' \
"$ROOT/include/cst_version.h" | paste -sd.)"
rm -rf "$OUT"
mkdir -p "$OUT"
cp -f "$EXE" "$OUT/cst-agent-app.exe"
"$STRIP" "$OUT/cst-agent-app.exe" 2>/dev/null || true
# Which DLLs the app needs: the exe's imports, then theirs, and so on. What
# Windows itself provides is skipped; anything else has to come out of the
# extracted packages, which is what decided the list of packages above.
missing=0
isSystem() { # isSystem DLLNAME
case "${1,,}" in
api-ms-win-*|kernel32.dll|user32.dll|advapi32.dll|shell32.dll|ws2_32.dll|\
iphlpapi.dll|bcrypt.dll|crypt32.dll|secur32.dll|ole32.dll|oleaut32.dll|\
gdi32.dll|comdlg32.dll|dwmapi.dll|uxtheme.dll|winmm.dll|dnsapi.dll|\
version.dll|shlwapi.dll|netapi32.dll|userenv.dll|mpr.dll|ntdll.dll|\
authz.dll|d3d11.dll|d3d12.dll|dwrite.dll|dxgi.dll|winhttp.dll|\
rpcrt4.dll|usp10.dll)
return 0 ;;
esac
return 1
}
copyDeps() { # copyDeps FILE...
local queue=("$@") f n src
while ((${#queue[@]})); do
f="${queue[0]}"
queue=("${queue[@]:1}")
while read -r n; do
[[ -n "$n" ]] || continue
isSystem "$n" && continue
[[ -e "$OUT/$n" ]] && continue
src=""
for c in "$PREFIX/bin/$n" "$PREFIX/lib/$n"; do
[[ -e "$c" ]] && src="$c" && break
done
if [[ -z "$src" ]]; then
echo "missing DLL: $n (needed by $(basename "$f"))" >&2
missing=1
continue
fi
cp -f "$src" "$OUT/$n"
queue+=("$OUT/$n")
done < <("$OBJDUMP" -p "$f" | awk '/DLL Name/{print $3}')
done
}
copyDeps "$OUT/cst-agent-app.exe"
# The plugins Qt loads by name: the window itself (and the offscreen one, so a
# headless run is possible), the image formats a transcript can carry, the SVG
# icon engine (the app's icons are .svg through QIcon).
mkdir -p "$OUT/plugins"
for p in platforms/qwindows.dll platforms/qoffscreen.dll \
iconengines/qsvgicon.dll; do
[[ -f "$PREFIX/share/qt6/plugins/$p" ]] || {
echo "no plugin $p" >&2
missing=1
continue
}
mkdir -p "$OUT/plugins/$(dirname "$p")"
cp -f "$PREFIX/share/qt6/plugins/$p" "$OUT/plugins/$p"
done
for p in imageformats/*.dll; do
[[ -e "$PREFIX/share/qt6/plugins/$p" ]] || continue
mkdir -p "$OUT/plugins/imageformats"
cp -f "$PREFIX/share/qt6/plugins/$p" "$OUT/plugins/imageformats/"
done
# The QML modules the window imports (QtQuick, QtQuick.Controls and its Basic
# style, Layouts, Window, ...): the whole tree, which is what a deployed Qt app
# has next to its exe, and what the QML import path picks up from <exe>/qml.
echo "==> QML modules"
cp -r "$PREFIX/share/qt6/qml" "$OUT/qml"
# ... and the plugins those QML modules load (a style, a helper, an image
# provider: each is a DLL with its own imports).
while IFS= read -r d; do
copyDeps "$d"
done < <(find "$OUT/qml" -name '*.dll')
# What the engine's shell tool wants beside it, exactly as the TUI package
# ships it: the static busybox for Windows is the shell the engine runs.
if [[ -f "$ROOT/third_party/busybox/busybox.exe" ]]; then
cp -f "$ROOT/third_party/busybox/busybox.exe" "$OUT/busybox.exe"
cp -f "$ROOT/third_party/busybox/LICENSE.busybox" "$OUT/" 2>/dev/null || true
fi
# Qt finds its plugins and QML imports relative to the exe through this file.
cat >"$OUT/qt.conf" <<'EOF'
[Paths]
Prefix = .
Plugins = plugins
Qml2Imports = qml
EOF
echo "$ver" >"$OUT/VERSION"
if [[ "$missing" != 0 ]]; then
echo "the package is incomplete (see the missing entries above)" >&2
exit 1
fi
zip="$ROOT/dist/cst-agent-app-$ver-windows-x86_64.zip"
rm -f "$zip"
(cd "$ROOT/dist" && zip -qr "$zip" windows-x86_64-app)
(cd "$ROOT/dist" && sha256sum "$(basename "$zip")" >"$(basename "$zip").sha256")
du -sh "$OUT"
ls -l "$zip"
echo "$zip"