From 500f7d2735d1262dff4e187693008685abcae8d4 Mon Sep 17 00:00:00 2001 From: mesalogo Date: Tue, 25 Aug 2026 18:41:35 +0800 Subject: [PATCH] feat: add experimental LoongArch desktop preview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GoodBuddy can now be cross-packaged as an isolated Debian loong64 preview with verified Electron, node-pty, and Koffi binaries. The preview remains separate from the standard release matrix and excludes Agent, Remote Runtime, OpenCode, and native Canvas until those dependencies support LoongArch. The Chinese website identifies GoodBuddy as the first open-source desktop AI assistant with LoongArch support while clearly labeling the independently built package as a preview. Release note: 新增龙芯 LoongArch 独立编译预览版,并提供完整构建文档;当前预览版不包含 Agent、Remote Runtime、OpenCode 和原生 Canvas。 --- AGENTS.md | 15 +- build/loongarch-cross/.dockerignore | 1 + build/loongarch-cross/Dockerfile | 54 ++++ build/loongarch-cross/README.md | 78 +++++ build/loongarch-cross/prepare-input.cjs | 108 +++++++ build/loongarch-cross/preview-deb.sh | 322 ++++++++++++++++++++ build/loongarch-cross/probe.sh | 314 +++++++++++++++++++ build/loongarch-cross/toolchain.cmake | 14 + docs/development/loongarch-preview-build.md | 251 +++++++++++++++ sites/README.md | 11 +- sites/index.html | 31 +- sites/scripts/validate.mjs | 2 + tests/loongarch-cross.test.ts | 108 +++++++ 13 files changed, 1294 insertions(+), 15 deletions(-) create mode 100644 build/loongarch-cross/.dockerignore create mode 100644 build/loongarch-cross/Dockerfile create mode 100644 build/loongarch-cross/README.md create mode 100644 build/loongarch-cross/prepare-input.cjs create mode 100644 build/loongarch-cross/preview-deb.sh create mode 100644 build/loongarch-cross/probe.sh create mode 100644 build/loongarch-cross/toolchain.cmake create mode 100644 docs/development/loongarch-preview-build.md create mode 100644 tests/loongarch-cross.test.ts diff --git a/AGENTS.md b/AGENTS.md index a0bc8cda..c94f6921 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -185,6 +185,13 @@ Release note: 修复左上角项目设置与消息通道项目设置不一致的 - `.github/workflows/packages.yml` is the canonical cross-platform packaging workflow. It validates and builds `out` once, then packages on six native runners: Windows, macOS, and Linux, each for x64 and arm64. +- The experimental Linux LoongArch (`loong64`) preview is built separately + through `build/loongarch-cross` by following + `docs/development/loongarch-preview-build.md`. It is not part of + `.github/workflows/packages.yml`, the six-runner GitHub Actions matrix, + standard release manifests, GitHub Release asset expectations, or the + production update index. Do not add it to those paths unless the user + explicitly requests a future production-support migration. - Run the unified packager with `npm run release:package -- --platform --arch `. It only packages for the native host and writes to @@ -271,7 +278,9 @@ Every version-tag release must follow this sequence. A branch-only push does not require release notes. 1. Confirm that the user wants a release tag and identify the exact release - commit and the new `package.json` version. + commit and the new `package.json` version. Separately ask whether this + release should also receive an experimental LoongArch preview build; never + infer that choice from an earlier release. 2. Find the latest stable version tag reachable before the release commit and inspect the complete commit and file diff from that tag to the release commit. For the first tagged release, inspect the relevant repository @@ -294,6 +303,10 @@ not require release notes. `npm run release:notes:verify`, the required source validators, the production build, and any native candidate launch probe available on the current host. The six native CI jobs remain the cross-platform authority. + If the user requested a LoongArch preview in step 1, build it separately + from this exact candidate by following the LoongArch build document, record + its size and SHA-256, and keep it outside the standard GitHub/OSS release + asset set unless the user separately authorizes publishing it. 7. Fetch both remotes immediately before tagging. Inspect any remote branch movement instead of overwriting or silently merging it. Confirm the working tree is clean, the candidate tag is unused locally and remotely, and the diff --git a/build/loongarch-cross/.dockerignore b/build/loongarch-cross/.dockerignore new file mode 100644 index 00000000..1a06816d --- /dev/null +++ b/build/loongarch-cross/.dockerignore @@ -0,0 +1 @@ +results diff --git a/build/loongarch-cross/Dockerfile b/build/loongarch-cross/Dockerfile new file mode 100644 index 00000000..c395a296 --- /dev/null +++ b/build/loongarch-cross/Dockerfile @@ -0,0 +1,54 @@ +FROM ubuntu:26.04 + +ARG DEBIAN_FRONTEND=noninteractive +ARG RUST_TOOLCHAIN=1.93.1 + +RUN apt-get update \ + && apt-get install --yes --no-install-recommends \ + binutils-loongarch64-linux-gnu \ + build-essential \ + ca-certificates \ + cmake \ + dpkg-dev \ + fakeroot \ + file \ + g++-loongarch64-linux-gnu \ + gcc-loongarch64-linux-gnu \ + libc6-dev-loong64-cross \ + make \ + ninja-build \ + node-gyp \ + nodejs \ + npm \ + pkg-config \ + python3 \ + qemu-user \ + rustup \ + tar \ + xz-utils \ + && rm -rf /var/lib/apt/lists/* + +RUN rustup set profile minimal \ + && rustup toolchain install "${RUST_TOOLCHAIN}" \ + --target loongarch64-unknown-linux-gnu + +ENV CC=loongarch64-linux-gnu-gcc \ + CXX=loongarch64-linux-gnu-g++ \ + AR=loongarch64-linux-gnu-ar \ + RANLIB=loongarch64-linux-gnu-ranlib \ + STRIP=loongarch64-linux-gnu-strip \ + CARGO_BUILD_TARGET=loongarch64-unknown-linux-gnu \ + CARGO_TARGET_LOONGARCH64_UNKNOWN_LINUX_GNU_LINKER=loongarch64-linux-gnu-gcc \ + RUSTUP_TOOLCHAIN=${RUST_TOOLCHAIN} + +COPY toolchain.cmake /opt/goodbuddy/loongarch64-toolchain.cmake +COPY probe.sh /usr/local/bin/goodbuddy-loongarch-cross-probe +COPY preview-deb.sh /usr/local/bin/goodbuddy-loongarch-preview-deb + +RUN chmod 0755 \ + /usr/local/bin/goodbuddy-loongarch-cross-probe \ + /usr/local/bin/goodbuddy-loongarch-preview-deb + +WORKDIR /work + +ENTRYPOINT ["/usr/local/bin/goodbuddy-loongarch-cross-probe"] diff --git a/build/loongarch-cross/README.md b/build/loongarch-cross/README.md new file mode 100644 index 00000000..06466382 --- /dev/null +++ b/build/loongarch-cross/README.md @@ -0,0 +1,78 @@ +# LoongArch cross-compilation probe + +This directory defines an isolated Ubuntu 26.04 amd64 environment for probing +GoodBuddy's Linux `loong64` prerequisites. It does not build Electron or +OpenCode. + +The image includes: + +- GCC and G++ targeting `loongarch64-linux-gnu` +- A LoongArch glibc development sysroot +- QEMU user-mode execution +- CMake and Ninja +- Rust's `loongarch64-unknown-linux-gnu` target +- Node.js, npm, and node-gyp + +The probe validates: + +- C, C++, CMake, and Rust LoongArch ELF output +- C, C++, and Rust execution through QEMU +- A minimal NAPI-RS Rust addon cross-build +- A Debian package with `Architecture: loong64` +- Koffi 3.1.4's published LoongArch glibc binding and the absence of a musl + binding +- The current npm availability of an `@napi-rs/canvas` LoongArch package +- A source cross-build of node-pty 1.1.0 + +The node-pty probe uses its upstream node-gyp 11.4.2 dependency. Ubuntu's +distribution node-gyp unconditionally links Linux addons with `-lnode`, which +is appropriate for Ubuntu's shared Node.js package but not for Electron native +addons. + +Build and run it from this directory on an amd64 Docker host: + +```sh +docker build \ + --tag goodbuddy-loongarch-cross:ubuntu-26.04 \ + . + +mkdir -p results +docker run --rm \ + --volume "$PWD/results:/results" \ + goodbuddy-loongarch-cross:ubuntu-26.04 +``` + +Each run writes to a new UTC timestamped directory below `results`. A passing +run contains `summary.txt`, `SHA256SUMS`, the generated ELF files, and the +probe DEB. + +QEMU only checks user-mode execution against the cross sysroot. Native Electron +loading, PTY behavior, desktop integration, GPU, audio, and packaging must +still be validated on a real Debian `loong64` host. + +Koffi 3.1.4 currently publishes only `linux_loong64/koffi.node`, not a +`musl_loong64` binding. That is sufficient for a Debian desktop package but +does not satisfy GoodBuddy Agent's current dual-glibc/musl bundle contract. + +The NAPI-RS probe confirms that a Rust Node addon can be cross-compiled. It +does not build Canvas itself, whose Skia dependency requires a separate port +and native validation. + +## Experimental desktop preview + +`prepare-input.cjs` creates the architecture-neutral GoodBuddy input archive. +`preview-deb.sh` combines it with the verified community Electron 42.3.0 +LoongArch shell and produces an isolated `goodbuddy-loongarch-preview` DEB. + +The preview: + +- uses a separate application name and user-data directory; +- includes the direct model path and Continue runtime; +- cross-builds node-pty against Electron 42.3.0; +- includes Koffi's LoongArch glibc binding; +- does not include GoodBuddy Agent, Remote Runtime, OpenCode, or native Canvas; +- is not part of the production release matrix. + +See +[`docs/development/loongarch-preview-build.md`](../../docs/development/loongarch-preview-build.md) +for the complete build and validation procedure. diff --git a/build/loongarch-cross/prepare-input.cjs b/build/loongarch-cross/prepare-input.cjs new file mode 100644 index 00000000..b100af20 --- /dev/null +++ b/build/loongarch-cross/prepare-input.cjs @@ -0,0 +1,108 @@ +const { + existsSync, + mkdirSync, + renameSync, + rmSync, + statSync +} = require('node:fs') +const { dirname, join, resolve } = require('node:path') +const tar = require('tar') + +const root = join(__dirname, '..', '..') +const defaultOutput = join( + root, + 'dist', + 'loongarch-cross', + 'goodbuddy-loongarch-cross-input.tgz' +) +const inputPaths = [ + 'LICENSE', + 'agent-runtime-lock.json', + 'package.json', + 'package-lock.json', + 'remote-runtime-lock.json', + 'out', + 'resources/agent-release-keys.json', + 'resources/release-notes.json', + 'resources/skills', + 'build/icon.png', + 'build/icon-tray.png', + 'node_modules/@continuedev/cli/package.json', + 'node_modules/@continuedev/cli/dist/cn.js', + 'node_modules/@continuedev/cli/dist/index.js', + 'node_modules/@continuedev/cli/dist/xhr-sync-worker.js', + 'node_modules/npm' +] + +function parseArguments(argv) { + const options = { output: defaultOutput } + for (let index = 0; index < argv.length; index += 1) { + const argument = argv[index] + if (argument !== '--output') { + throw new Error(`未知参数:${argument}`) + } + const value = argv[index + 1] + if (!value) { + throw new Error('--output 缺少值') + } + options.output = resolve(value) + index += 1 + } + return options +} + +function verifyInputs(projectRoot = root) { + for (const relativePath of inputPaths) { + if (!existsSync(join(projectRoot, relativePath))) { + throw new Error(`龙芯交叉构建输入缺失:${relativePath}`) + } + } +} + +async function createInputArchive(outputPath, projectRoot = root) { + verifyInputs(projectRoot) + const resolvedOutput = resolve(outputPath) + const temporaryOutput = `${resolvedOutput}.partial-${process.pid}` + mkdirSync(dirname(resolvedOutput), { recursive: true }) + rmSync(temporaryOutput, { force: true }) + try { + await tar.c( + { + cwd: projectRoot, + file: temporaryOutput, + gzip: true, + portable: true, + noMtime: true + }, + inputPaths + ) + if (!statSync(temporaryOutput).isFile()) { + throw new Error('龙芯交叉构建输入归档未生成') + } + rmSync(resolvedOutput, { force: true }) + renameSync(temporaryOutput, resolvedOutput) + } finally { + rmSync(temporaryOutput, { force: true }) + } + return resolvedOutput +} + +async function main(argv = process.argv.slice(2)) { + const options = parseArguments(argv) + const output = await createInputArchive(options.output) + console.log(`龙芯交叉构建输入已生成:${output}`) +} + +module.exports = { + createInputArchive, + inputPaths, + parseArguments, + verifyInputs +} + +if (require.main === module) { + main().catch((error) => { + console.error(error) + process.exitCode = 1 + }) +} diff --git a/build/loongarch-cross/preview-deb.sh b/build/loongarch-cross/preview-deb.sh new file mode 100644 index 00000000..1d1640c9 --- /dev/null +++ b/build/loongarch-cross/preview-deb.sh @@ -0,0 +1,322 @@ +#!/usr/bin/env bash + +set -euo pipefail + +input_archive="${1:?missing GoodBuddy input archive}" +electron_archive="${2:?missing Electron loong64 archive}" +output_root="${3:-/preview-output}" + +electron_version="42.3.0" +electron_sha256="92b0ca0c9c18ed90166918a4ac1970266c4fa967aee9277031b3b250b905526e" +koffi_version="3.1.4" +node_pty_version="1.1.0" +node_gyp_version="11.4.2" +preview_iteration="1" +application_directory="GoodBuddy-LoongArch-Preview" +executable_name="goodbuddy-loongarch-preview" + +assert_file() { + if ! test -f "$1"; then + echo "missing required file: $1" >&2 + exit 1 + fi +} + +assert_loongarch_elf() { + local file_path="$1" + assert_file "${file_path}" + file "${file_path}" + readelf -h "${file_path}" | grep -Eq \ + 'Machine:[[:space:]]+(LoongArch|Loongson Loongarch)' +} + +assert_file "${input_archive}" +assert_file "${electron_archive}" +mkdir -p "${output_root}" + +actual_electron_sha256="$( + sha256sum "${electron_archive}" | cut -d ' ' -f 1 +)" +if test "${actual_electron_sha256}" != "${electron_sha256}"; then + echo "Electron loong64 archive digest mismatch" >&2 + exit 1 +fi + +run_id="$(date -u +%Y%m%dT%H%M%SZ)" +work_root="${output_root}/.work-${run_id}" +input_root="${work_root}/input" +package_root="${work_root}/package" +application_root="${package_root}/opt/${application_directory}" +resources_root="${application_root}/resources" +app_root="${resources_root}/app" +artifact_root="${output_root}/${run_id}" + +if test -e "${work_root}" || test -e "${artifact_root}"; then + echo "refusing to reuse preview build directory" >&2 + exit 1 +fi + +cleanup() { + rm -rf "${work_root}" +} +trap cleanup EXIT + +mkdir -p "${input_root}" "${app_root}" "${artifact_root}" +tar -xzf "${input_archive}" -C "${input_root}" + +project_version="$( + node -p \ + "require('${input_root}/package.json').version" +)" +preview_version="${project_version}-loong64-preview.${preview_iteration}" +deb_version="${project_version}~loong64preview${preview_iteration}" +artifact_name="GoodBuddy-${project_version}-linux-loong64-preview.deb" + +echo "== extract verified Electron shell ==" +python3 -m zipfile -e "${electron_archive}" "${application_root}" +assert_loongarch_elf "${application_root}/electron" +mv "${application_root}/electron" \ + "${application_root}/${executable_name}" +chmod 0755 "${application_root}/${executable_name}" +rm -f "${resources_root}/default_app.asar" +if test -f "${application_root}/chrome-sandbox"; then + chmod 4755 "${application_root}/chrome-sandbox" +fi + +echo "== install production JavaScript dependencies ==" +cp "${input_root}/package.json" "${app_root}/package.json" +cp "${input_root}/package-lock.json" "${app_root}/package-lock.json" +( + cd "${app_root}" + npm ci \ + --ignore-scripts \ + --omit=dev \ + --cpu=loong64 \ + --os=linux +) + +rm -rf \ + "${app_root}/node_modules/@esbuild" \ + "${app_root}/node_modules/@img" \ + "${app_root}/node_modules/@rollup" +find "${app_root}/node_modules/@koromix" \ + -mindepth 1 \ + -maxdepth 1 \ + -type d \ + -exec rm -rf -- {} + 2>/dev/null || true +find "${app_root}/node_modules/@napi-rs" \ + -mindepth 1 \ + -maxdepth 1 \ + -type d \ + -name 'canvas-*' \ + -exec rm -rf -- {} + 2>/dev/null || true + +node - "${input_root}" "${app_root}/package.json" \ + "${preview_version}" <<'NODE' +const { readFileSync, writeFileSync } = require('node:fs') +const [inputRoot, outputPath, version] = process.argv.slice(2) +const metadata = JSON.parse( + readFileSync(`${inputRoot}/package.json`, 'utf8') +) +metadata.name = 'goodbuddy-loongarch-preview' +metadata.productName = 'GoodBuddy LoongArch Preview' +metadata.desktopName = 'GoodBuddy LoongArch Preview' +metadata.version = version +metadata.main = './out/main/index.js' +delete metadata.build +delete metadata.devDependencies +delete metadata.optionalDependencies +writeFileSync(outputPath, `${JSON.stringify(metadata, null, 2)}\n`) +NODE +rm -f "${app_root}/package-lock.json" + +echo "== stage target Koffi binding ==" +koffi_stage="${work_root}/koffi" +mkdir -p "${koffi_stage}" +koffi_pack_json="$( + npm pack \ + "@koromix/koffi-linux-loong64@${koffi_version}" \ + --ignore-scripts \ + --json \ + --pack-destination "${koffi_stage}" +)" +koffi_archive="$( + node -e \ + 'const value=JSON.parse(process.argv[1]); process.stdout.write(value[0].filename)' \ + "${koffi_pack_json}" +)" +koffi_integrity="$( + node -p \ + "require('${input_root}/package-lock.json').packages['node_modules/@koromix/koffi-linux-loong64'].integrity" +)" +node - "${koffi_stage}/${koffi_archive}" \ + "${koffi_integrity}" <<'NODE' +const { createHash } = require('node:crypto') +const { readFileSync } = require('node:fs') +const [filePath, integrity] = process.argv.slice(2) +const [algorithm, expected] = integrity.split('-', 2) +const actual = createHash(algorithm) + .update(readFileSync(filePath)) + .digest('base64') +if (actual !== expected) { + throw new Error('Koffi archive integrity mismatch') +} +NODE +koffi_target="${app_root}/node_modules/@koromix/koffi-linux-loong64" +mkdir -p "${koffi_target}" +tar \ + -xzf "${koffi_stage}/${koffi_archive}" \ + -C "${koffi_target}" \ + --strip-components 1 +koffi_binding="${koffi_target}/linux_loong64/koffi.node" +assert_loongarch_elf "${koffi_binding}" + +echo "== cross-build node-pty for Electron ${electron_version} ==" +node_gyp_root="${work_root}/node-gyp" +npm install \ + --ignore-scripts \ + --prefix "${node_gyp_root}" \ + "node-gyp@${node_gyp_version}" +node_pty_root="${app_root}/node_modules/node-pty" +test "$( + node -p "require('${node_pty_root}/package.json').version" +)" = "${node_pty_version}" +rm -rf "${node_pty_root}/build" "${node_pty_root}/prebuilds" +( + cd "${node_pty_root}" + CC=loongarch64-linux-gnu-gcc \ + CXX=loongarch64-linux-gnu-g++ \ + npm_config_arch=loong64 \ + npm_config_target_arch=loong64 \ + npm_config_runtime=electron \ + npm_config_target="${electron_version}" \ + npm_config_disturl=https://electronjs.org/headers \ + "${node_gyp_root}/node_modules/.bin/node-gyp" rebuild \ + --arch=loong64 +) +node_pty_binding="${node_pty_root}/build/Release/pty.node" +assert_loongarch_elf "${node_pty_binding}" + +echo "== stage GoodBuddy application and architecture-neutral runtimes ==" +cp -a "${input_root}/out" "${app_root}/out" +mkdir -p \ + "${resources_root}/licenses" \ + "${resources_root}/runtimes" \ + "${resources_root}/skills" +cp -a "${input_root}/resources/skills/." "${resources_root}/skills/" +cp "${input_root}/resources/release-notes.json" \ + "${resources_root}/release-notes.json" +cp "${input_root}/resources/agent-release-keys.json" \ + "${resources_root}/agent-release-keys.json" +cp "${input_root}/agent-runtime-lock.json" \ + "${resources_root}/agent-runtime-lock.json" +cp "${input_root}/remote-runtime-lock.json" \ + "${resources_root}/remote-runtime-lock.json" +cp "${input_root}/build/icon.png" "${resources_root}/icon.png" +cp "${input_root}/build/icon-tray.png" \ + "${resources_root}/tray-icon.png" +cp "${input_root}/LICENSE" \ + "${resources_root}/licenses/GoodBuddy-0BSD.txt" +mkdir -p "${resources_root}/runtimes/continue/dist" +cp "${input_root}/node_modules/@continuedev/cli/package.json" \ + "${resources_root}/runtimes/continue/package.json" +cp "${input_root}/node_modules/@continuedev/cli/dist/cn.js" \ + "${resources_root}/runtimes/continue/dist/cn.js" +cp "${input_root}/node_modules/@continuedev/cli/dist/index.js" \ + "${resources_root}/runtimes/continue/dist/index.js" +cp "${input_root}/node_modules/@continuedev/cli/dist/xhr-sync-worker.js" \ + "${resources_root}/runtimes/continue/dist/xhr-sync-worker.js" +cp -a "${input_root}/node_modules/npm" \ + "${resources_root}/runtimes/npm" + +echo "== reject foreign native addons ==" +while IFS= read -r -d '' native_addon; do + assert_loongarch_elf "${native_addon}" +done < <( + find "${app_root}/node_modules" \ + -type f \ + -name '*.node' \ + -print0 +) + +if test -e "${resources_root}/agents" || + test -e "${resources_root}/remote-runtimes" || + test -e "${resources_root}/runtimes/opencode" +then + echo "preview package unexpectedly contains Agent or OpenCode payload" >&2 + exit 1 +fi + +echo "== build isolated preview DEB ==" +mkdir -p \ + "${package_root}/DEBIAN" \ + "${package_root}/usr/bin" \ + "${package_root}/usr/share/applications" \ + "${package_root}/usr/share/icons/hicolor/512x512/apps" +ln -s \ + "/opt/${application_directory}/${executable_name}" \ + "${package_root}/usr/bin/${executable_name}" +cp "${input_root}/build/icon.png" \ + "${package_root}/usr/share/icons/hicolor/512x512/apps/${executable_name}.png" +cat > "${package_root}/usr/share/applications/${executable_name}.desktop" < "${package_root}/DEBIAN/control" < "${artifact_root}/SHA256SUMS" +cat > "${artifact_root}/preview-manifest.json" < >(tee "${log}") 2>&1 + +assert_loongarch_elf() { + local file_path="$1" + file "${file_path}" + readelf -h "${file_path}" | grep -Eq \ + 'Machine:[[:space:]]+(LoongArch|Loongson Loongarch)' +} + +echo "== tool versions ==" +loongarch64-linux-gnu-gcc --version | head -n 1 +loongarch64-linux-gnu-g++ --version | head -n 1 +loongarch64-linux-gnu-ld --version | head -n 1 +qemu-loongarch64 --version | head -n 1 +node --version +npm --version +node-gyp --version +rustc --version +cargo --version +cmake --version | head -n 1 + +echo "== C probe ==" +cat > "${results}/hello.c" <<'EOF' +#include + +int main(void) { + puts("goodbuddy-loongarch-c-ok"); + return 0; +} +EOF + +loongarch64-linux-gnu-gcc \ + -O2 \ + -Wall \ + -Wextra \ + -Werror \ + "${results}/hello.c" \ + -o "${results}/hello-c" +assert_loongarch_elf "${results}/hello-c" +test "$( + qemu-loongarch64 \ + -L /usr/loongarch64-linux-gnu \ + "${results}/hello-c" +)" = "goodbuddy-loongarch-c-ok" + +echo "== C++ probe ==" +cat > "${results}/hello.cpp" <<'EOF' +#include +#include + +int main() { + constexpr std::string_view message{"goodbuddy-loongarch-cpp-ok"}; + std::cout << message << '\n'; + return 0; +} +EOF + +loongarch64-linux-gnu-g++ \ + -std=c++20 \ + -O2 \ + -Wall \ + -Wextra \ + -Werror \ + "${results}/hello.cpp" \ + -o "${results}/hello-cpp" +assert_loongarch_elf "${results}/hello-cpp" +test "$( + qemu-loongarch64 \ + -L /usr/loongarch64-linux-gnu \ + "${results}/hello-cpp" +)" = "goodbuddy-loongarch-cpp-ok" + +echo "== CMake probe ==" +mkdir "${results}/cmake-source" +cat > "${results}/cmake-source/CMakeLists.txt" <<'EOF' +cmake_minimum_required(VERSION 3.20) +project(goodbuddy_loongarch_probe LANGUAGES C) +add_executable(goodbuddy-cmake-probe main.c) +target_compile_options( + goodbuddy-cmake-probe + PRIVATE -Wall -Wextra -Werror +) +EOF +cp "${results}/hello.c" "${results}/cmake-source/main.c" +cmake \ + -S "${results}/cmake-source" \ + -B "${results}/cmake-build" \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_TOOLCHAIN_FILE=/opt/goodbuddy/loongarch64-toolchain.cmake +cmake --build "${results}/cmake-build" +assert_loongarch_elf \ + "${results}/cmake-build/goodbuddy-cmake-probe" + +echo "== Rust probe ==" +mkdir -p "${results}/rust-probe/src" +cat > "${results}/rust-probe/Cargo.toml" <<'EOF' +[package] +name = "goodbuddy-loongarch-rust-probe" +version = "0.1.0" +edition = "2024" + +[dependencies] +EOF +cat > "${results}/rust-probe/src/main.rs" <<'EOF' +fn main() { + println!("goodbuddy-loongarch-rust-ok"); +} +EOF +cargo build \ + --release \ + --manifest-path "${results}/rust-probe/Cargo.toml" +rust_binary="$( + find "${results}/rust-probe/target" \ + -type f \ + -name goodbuddy-loongarch-rust-probe \ + -print \ + -quit +)" +test -n "${rust_binary}" +assert_loongarch_elf "${rust_binary}" +test "$( + qemu-loongarch64 \ + -L /usr/loongarch64-linux-gnu \ + "${rust_binary}" +)" = "goodbuddy-loongarch-rust-ok" + +echo "== NAPI-RS Rust addon probe ==" +mkdir -p "${results}/napi-rs-probe/src" +cat > "${results}/napi-rs-probe/Cargo.toml" <<'EOF' +[package] +name = "goodbuddy-loongarch-napi-probe" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +napi = { version = "3", default-features = false, features = ["napi8"] } +napi-derive = "3" + +[build-dependencies] +napi-build = "2" +EOF +cat > "${results}/napi-rs-probe/build.rs" <<'EOF' +fn main() { + napi_build::setup(); +} +EOF +cat > "${results}/napi-rs-probe/src/lib.rs" <<'EOF' +use napi_derive::napi; + +#[napi] +pub fn architecture_probe() -> &'static str { + "loong64" +} +EOF +cargo build \ + --release \ + --manifest-path "${results}/napi-rs-probe/Cargo.toml" +napi_binding="$( + find "${results}/napi-rs-probe/target" \ + -type f \ + -name 'libgoodbuddy_loongarch_napi_probe.so' \ + -print \ + -quit +)" +test -n "${napi_binding}" +assert_loongarch_elf "${napi_binding}" + +echo "== Debian package probe ==" +package_root="${results}/deb-root" +mkdir -p \ + "${package_root}/DEBIAN" \ + "${package_root}/usr/lib/goodbuddy-loongarch-probe" +cp "${results}/hello-c" \ + "${package_root}/usr/lib/goodbuddy-loongarch-probe/hello" +cat > "${package_root}/DEBIAN/control" <<'EOF' +Package: goodbuddy-loongarch-cross-probe +Version: 1.0.0 +Architecture: loong64 +Maintainer: MesaLogo +Description: GoodBuddy LoongArch cross-compilation probe +EOF +dpkg-deb --build \ + "${package_root}" \ + "${results}/goodbuddy-loongarch-cross-probe_1.0.0_loong64.deb" +test "$( + dpkg-deb \ + --field \ + "${results}/goodbuddy-loongarch-cross-probe_1.0.0_loong64.deb" \ + Architecture +)" = "loong64" + +echo "== Koffi prebuilt probe ==" +mkdir "${results}/koffi" +npm pack \ + "@koromix/koffi-linux-loong64@3.1.4" \ + --ignore-scripts \ + --pack-destination "${results}/koffi" +koffi_archive="$( + find "${results}/koffi" \ + -maxdepth 1 \ + -type f \ + -name '*.tgz' \ + -print \ + -quit +)" +test -n "${koffi_archive}" +mkdir "${results}/koffi/package" +tar \ + -xzf "${koffi_archive}" \ + -C "${results}/koffi/package" \ + --strip-components 1 +koffi_binding="${results}/koffi/package/linux_loong64/koffi.node" +assert_loongarch_elf "${koffi_binding}" +if test -e "${results}/koffi/package/musl_loong64/koffi.node"; then + echo "Unexpected Koffi musl binding is present" + exit 1 +fi +echo "Koffi loong64 currently provides a glibc binding only" + +echo "== Canvas package availability probe ==" +canvas_package_status=unavailable +if npm view \ + "@napi-rs/canvas-linux-loong64-gnu" \ + version \ + > "${results}/canvas-package-version.txt" \ + 2> "${results}/canvas-package-error.txt" +then + canvas_package_status=available +fi +echo "@napi-rs/canvas loong64 package: ${canvas_package_status}" + +echo "== node-pty cross-build probe ==" +mkdir "${results}/node-pty" +npm pack \ + "node-pty@1.1.0" \ + --ignore-scripts \ + --pack-destination "${results}/node-pty" +node_pty_archive="$( + find "${results}/node-pty" \ + -maxdepth 1 \ + -type f \ + -name '*.tgz' \ + -print \ + -quit +)" +test -n "${node_pty_archive}" +mkdir "${results}/node-pty/package" +tar \ + -xzf "${node_pty_archive}" \ + -C "${results}/node-pty/package" \ + --strip-components 1 +( + cd "${results}/node-pty/package" + npm install --ignore-scripts --omit=dev + npm install \ + --ignore-scripts \ + --no-save \ + "node-gyp@11.4.2" + CC=loongarch64-linux-gnu-gcc \ + CXX=loongarch64-linux-gnu-g++ \ + npm_config_arch=loong64 \ + npm_config_target_arch=loong64 \ + npm_config_runtime=electron \ + npm_config_target=43.2.0 \ + npm_config_disturl=https://electronjs.org/headers \ + ./node_modules/.bin/node-gyp rebuild \ + --arch=loong64 +) +node_pty_binding="${results}/node-pty/package/build/Release/pty.node" +assert_loongarch_elf "${node_pty_binding}" + +echo "== summary ==" +sha256sum \ + "${results}/hello-c" \ + "${results}/hello-cpp" \ + "${rust_binary}" \ + "${napi_binding}" \ + "${results}/goodbuddy-loongarch-cross-probe_1.0.0_loong64.deb" \ + "${koffi_binding}" \ + "${node_pty_binding}" \ + > "${results}/SHA256SUMS" + +cat > "${results}/summary.txt" <-linux-loong64-preview.deb +preview-manifest.json +SHA256SUMS +``` + +脚本会执行以下强制检查: + +1. 校验 Electron zip 的固定 SHA-256。 +2. 删除 npm 错误解析出的宿主架构可选原生包。 +3. 按 lockfile integrity 校验并安装 Koffi LoongArch glibc binding。 +4. 使用上游 node-gyp 11.4.2 和 Electron 42.3.0 headers 交叉编译 node-pty。 +5. 拒绝 `node_modules` 中任何不是 LoongArch ELF 的 `.node` 文件。 +6. 拒绝意外进入包内的 Agent、Remote Runtime 或 OpenCode 目录。 +7. 生成 `Architecture: loong64` 的 DEB,解包后再次校验关键 ELF。 + +Ubuntu 发行版自带的 node-gyp 会在 Linux addon 链接阶段无条件添加 +`-lnode`,不适合此 Electron 交叉编译;不要用它替换脚本固定的上游 +node-gyp。 + +## 验证边界 + +### 已由交叉构建验证 + +- C、C++、CMake 和 Rust 生成 LoongArch ELF +- C、C++ 和 Rust 程序通过 QEMU user mode 执行 +- 最小 NAPI-RS addon 交叉编译 +- node-pty 针对 Electron headers 交叉编译 +- Koffi LoongArch glibc binding 架构 +- Electron、node-pty、Koffi 和 DEB `loong64` 元数据 +- DEB 完整解包 +- Debian Forky rootfs 中 Electron 全部共享库解析;该检查发现并补充了 + `libpulse0` DEB 依赖 + +### 仍需真实龙芯桌面验证 + +1. 安装 DEB 并从桌面入口启动。 +2. 验证窗口、渲染、GPU 回退、输入法、系统托盘和通知。 +3. 配置模型并执行一次真实的直接模型对话。 +4. 验证 Continue Runtime 启动、取消和退出清理。 +5. 打开本地终端,验证 node-pty 输入、输出、窗口缩放和进程回收。 +6. 验证 Koffi 使用路径。 +7. 验证音频、文件选择器、剪贴板和外部链接。 +8. 确认 Agent、Remote Runtime、OpenCode 和 Canvas 相关入口不会被误认为 + 预览版已支持。 + +QEMU 可以补充动态链接器和缺库检查,但不能替代上述 GUI、PTY、GPU、音频和 +桌面集成真机测试。当前 Electron 在 QEMU user mode 下可以完成全部共享库 +解析;继续进入 Chromium 多线程初始化后,QEMU 出现 target `SIGSEGV`,因此 +不能用该结果判断真机 Electron 是否可以启动。 + +## 已知依赖问题 + +- npm 的 `--cpu=loong64 --os=linux` 仍可能安装宿主 x64 的 Koffi 可选包。 + 打包脚本会删除所有该作用域的已解析原生包,并按 lockfile integrity 手工 + 安装 LoongArch 包。 +- `@napi-rs/canvas` 没有发布 LoongArch 原生包。基础 NAPI-RS 探针通过只说明 + Rust addon 工具链可用,不代表 Skia/Canvas 已完成移植。 +- Koffi 没有 LoongArch musl binding,因此当前不能满足 GoodBuddy Agent 的 + 双 glibc/musl 产物契约。 +- 预览 Electron 比正式 GoodBuddy 低一个主版本。正式支持必须先获得与当前 + Electron 基线一致、可重复构建并完成安全维护的 LoongArch shell。 diff --git a/sites/README.md b/sites/README.md index 694f9569..316a92e1 100644 --- a/sites/README.md +++ b/sites/README.md @@ -4,11 +4,12 @@ 正式站点地址: -首页将 GoodBuddy 定位为“免注册、支持信创软硬件的一站式 AI 助手”, -优先展示三大桌面系统与双架构下载入口、统一 Agent Runtime,以及知识库、 -魔法笔记、智能心跳、桌面上下文和远程消息通道等桌面助手能力。下载区位于 -主要功能说明之前,并明确列出统信 UOS、银河麒麟、海光、兆芯、鲲鹏和飞腾 -对应的 Linux x64 / arm64 包。页面不重复设置底部下载推广区。 +首页将 GoodBuddy 定位为“首个支持龙芯的开源桌面 AI 助理产品”和 +“免注册、支持信创软硬件的一站式 AI 助手”,优先展示三大桌面系统、正式 +x64 / arm64 下载入口、独立编译的龙芯 loong64 预览版、统一 Agent Runtime, +以及知识库、魔法笔记、智能心跳、桌面上下文和远程消息通道等桌面助手能力。 +下载区位于主要功能说明之前,并明确列出统信 UOS、银河麒麟、海光、兆芯、 +鲲鹏、飞腾和龙芯。页面不重复设置底部下载推广区。 英文页面位于 `en.html`,不展示信创适配文案,三个平台的下载按钮始终前往 GitHub 最新正式 Release。 首屏产品界面默认正面展示,在精确指针设备上使用克制的 3D 倾斜、 diff --git a/sites/index.html b/sites/index.html index 23ab243f..a48199dd 100644 --- a/sites/index.html +++ b/sites/index.html @@ -5,14 +5,14 @@ - GoodBuddy|免注册、支持信创软硬件的一站式 AI 助手 + GoodBuddy|首个支持龙芯的开源桌面 AI 助理产品
- Windows · macOS · Linux + Windows · macOS · Linux · 龙芯预览版

免注册,
@@ -124,7 +124,9 @@ 一站式 AI 助手。

- 作为桌面助手与 AI 编程工具台,GoodBuddy 管理对话、知识、笔记与任务, + GoodBuddy 是首个支持龙芯的开源桌面 AI 助理产品,龙芯 LoongArch + 当前以独立编译预览版提供。作为桌面助手与 AI 编程工具台,GoodBuddy + 管理对话、知识、笔记与任务, 也通过独创的统一 Agent Runtime 接入直连模型、OpenCode、Continue 和 DeepSeek Harness。 无需反复配置命令行,选择工具和项目即可开始。

@@ -137,6 +139,10 @@ 3 大桌面系统,x64 / arm64 +
  • + + 龙芯 LoongArch 独立编译预览版 +
  • 4 类 Agent Runtime 统一接入 @@ -234,7 +240,7 @@
    4 类 Runtime多种 AI 编程路径
    3 大系统Windows / macOS / Linux
    -
    2 种架构x64 / arm64
    +
    3 种架构x64 / arm64 / loong64 预览
    1 个入口选择、配置、运行、审计
    @@ -347,14 +353,21 @@ @@ -512,7 +525,7 @@ GoodBuddy -

    免注册、支持信创软硬件的一站式 AI 助手

    +

    首个支持龙芯的开源桌面 AI 助理产品