feat: add experimental LoongArch desktop preview
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。
This commit is contained in:
@@ -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 <platform> --arch <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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
results
|
||||
@@ -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"]
|
||||
@@ -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.
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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" <<EOF
|
||||
[Desktop Entry]
|
||||
Name=GoodBuddy LoongArch Preview
|
||||
Comment=GoodBuddy LoongArch 主体预览版
|
||||
Exec=/opt/${application_directory}/${executable_name} %U
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=${executable_name}
|
||||
Categories=Utility;
|
||||
StartupWMClass=GoodBuddy LoongArch Preview
|
||||
EOF
|
||||
cat > "${package_root}/DEBIAN/control" <<EOF
|
||||
Package: goodbuddy-loongarch-preview
|
||||
Version: ${deb_version}
|
||||
Architecture: loong64
|
||||
Maintainer: MesaLogo
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Depends: libasound2t64, libatspi2.0-0t64, libgbm1, libgtk-3-0t64, libnotify4, libnss3, libpulse0, libsecret-1-0, libuuid1, libxss1, libxtst6, xdg-utils
|
||||
Description: GoodBuddy LoongArch desktop preview
|
||||
Experimental LoongArch desktop build without bundled Agent, Remote Runtime,
|
||||
OpenCode, or native Canvas image decoding.
|
||||
EOF
|
||||
|
||||
artifact="${artifact_root}/${artifact_name}"
|
||||
dpkg-deb --root-owner-group --build "${package_root}" "${artifact}"
|
||||
|
||||
echo "== verify preview package =="
|
||||
test "$(dpkg-deb --field "${artifact}" Architecture)" = "loong64"
|
||||
verify_root="${work_root}/verify"
|
||||
mkdir "${verify_root}"
|
||||
dpkg-deb --extract "${artifact}" "${verify_root}"
|
||||
assert_loongarch_elf \
|
||||
"${verify_root}/opt/${application_directory}/${executable_name}"
|
||||
assert_loongarch_elf \
|
||||
"${verify_root}/opt/${application_directory}/resources/app/${node_pty_binding#${app_root}/}"
|
||||
assert_loongarch_elf \
|
||||
"${verify_root}/opt/${application_directory}/resources/app/${koffi_binding#${app_root}/}"
|
||||
|
||||
sha256sum "${artifact}" > "${artifact_root}/SHA256SUMS"
|
||||
cat > "${artifact_root}/preview-manifest.json" <<EOF
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"product": "GoodBuddy LoongArch Preview",
|
||||
"goodBuddyVersion": "${project_version}",
|
||||
"previewVersion": "${preview_version}",
|
||||
"electronVersion": "${electron_version}",
|
||||
"architecture": "loong64",
|
||||
"artifact": "${artifact_name}",
|
||||
"bundledAgent": false,
|
||||
"bundledRemoteRuntime": false,
|
||||
"bundledOpenCode": false,
|
||||
"nativeCanvas": false,
|
||||
"continueRuntime": true,
|
||||
"nodePty": true,
|
||||
"koffiGlibc": true,
|
||||
"nativeLaunchVerified": false
|
||||
}
|
||||
EOF
|
||||
|
||||
cat "${artifact_root}/preview-manifest.json"
|
||||
echo "artifact=${artifact}"
|
||||
@@ -0,0 +1,314 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
results_root="${1:-/results}"
|
||||
run_id="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
results="${results_root}/${run_id}"
|
||||
mkdir -p "${results}"
|
||||
|
||||
log="${results}/probe.log"
|
||||
exec > >(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 <stdio.h>
|
||||
|
||||
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 <iostream>
|
||||
#include <string_view>
|
||||
|
||||
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" <<EOF
|
||||
result=pass
|
||||
target=loong64
|
||||
c=pass
|
||||
cpp=pass
|
||||
cmake=pass
|
||||
rust=pass
|
||||
napi_rs=pass
|
||||
qemu=pass
|
||||
deb=pass
|
||||
koffi=pass
|
||||
koffi_musl=unavailable
|
||||
canvas_package=${canvas_package_status}
|
||||
node_pty=pass
|
||||
EOF
|
||||
|
||||
cat "${results}/summary.txt"
|
||||
echo "results=${results}"
|
||||
@@ -0,0 +1,14 @@
|
||||
set(CMAKE_SYSTEM_NAME Linux)
|
||||
set(CMAKE_SYSTEM_PROCESSOR loongarch64)
|
||||
|
||||
set(CMAKE_C_COMPILER loongarch64-linux-gnu-gcc)
|
||||
set(CMAKE_CXX_COMPILER loongarch64-linux-gnu-g++)
|
||||
set(CMAKE_AR loongarch64-linux-gnu-ar)
|
||||
set(CMAKE_RANLIB loongarch64-linux-gnu-ranlib)
|
||||
set(CMAKE_STRIP loongarch64-linux-gnu-strip)
|
||||
|
||||
set(CMAKE_FIND_ROOT_PATH /usr/loongarch64-linux-gnu)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||||
@@ -0,0 +1,251 @@
|
||||
# GoodBuddy 龙芯(LoongArch)预览版构建
|
||||
|
||||
本文档是 GoodBuddy 龙芯预览版的权威构建与功能状态说明。当前目标是
|
||||
Debian `loong64` 桌面,不代表 GoodBuddy 已将龙芯纳入正式发布支持矩阵。
|
||||
|
||||
## 当前状态
|
||||
|
||||
当前流程可以在 AMD64 Linux 主机上交叉构建一个独立的
|
||||
`goodbuddy-loongarch-preview` DEB,并校验包内 Electron、node-pty 和 Koffi
|
||||
均为 LoongArch ELF。
|
||||
|
||||
预览版使用社区维护的 Electron 42.3.0 LoongArch 构建。GoodBuddy 正式版本
|
||||
当前使用 Electron 43.2.0,因此该预览包只用于移植和真机验证,不能作为正式
|
||||
发布基线。Electron 官方目前也不提供 Linux LoongArch 预编译包。
|
||||
|
||||
已完成的实际构建结果:
|
||||
|
||||
| 项目 | 结果 |
|
||||
| --- | --- |
|
||||
| GoodBuddy 源版本 | `0.11.0` |
|
||||
| 预览版本 | `0.11.0-loong64-preview.1` |
|
||||
| Debian 架构 | `loong64` |
|
||||
| Electron | 社区版 `42.3.0` |
|
||||
| DEB 包名 | `goodbuddy-loongarch-preview` |
|
||||
| 构建产物 | `GoodBuddy-0.11.0-linux-loong64-preview.deb` |
|
||||
| 已验证构建时间(UTC) | `20260825T102937Z` |
|
||||
| 已验证产物大小 | `187053208` bytes |
|
||||
| 已验证产物 SHA-256 | `461ade49f10a20ef7b28163077a2ecd6afd2c5b1b0befc673c6d174fd58497b3` |
|
||||
| DEB 解包、架构和动态库解析 | 已通过 |
|
||||
| QEMU Electron 初始化 | 共享库解析通过;Chromium 多线程初始化在 QEMU 下异常退出 |
|
||||
| 龙芯真机启动 | 尚未验证 |
|
||||
|
||||
版本号和产物名会跟随当前 `package.json` 版本变化,上表只记录首次成功构建。
|
||||
|
||||
## 功能列表
|
||||
|
||||
“已打包”只表示所需代码或组件进入 DEB,不等同于已在龙芯桌面完成运行验证。
|
||||
|
||||
| 功能 | 预览包状态 | 当前验证 |
|
||||
| --- | --- | --- |
|
||||
| GoodBuddy Electron 桌面主体 | 已打包 | Electron 可执行文件为 LoongArch ELF;尚未完成真机 GUI 启动 |
|
||||
| React 界面和主进程业务代码 | 已打包 | 生产 bundle 构建、测试、类型检查和 lint 已通过 |
|
||||
| 直接模型调用路径 | 已打包 | JavaScript 代码与生产依赖已包含;待龙芯真机端到端验证 |
|
||||
| Continue Runtime | 已打包 | CLI JavaScript 入口及 npm Runtime 已包含;待真机执行验证 |
|
||||
| 本地终端能力 | 已打包 | node-pty 1.1.0 已针对 Electron 42.3.0 交叉编译并验证为 LoongArch ELF;待真机 PTY 验证 |
|
||||
| Koffi glibc 原生绑定 | 已打包 | Koffi 3.1.4 LoongArch glibc binding 已校验为 LoongArch ELF |
|
||||
| 内置 Skills | 已打包 | 静态资源已进入 DEB |
|
||||
| GoodBuddy Agent | **不包含** | 当前 Agent 的正式构建和双 glibc/musl 产物契约不支持 LoongArch |
|
||||
| Remote Runtime | **不包含** | 当前没有 LoongArch Remote Runtime 产物 |
|
||||
| OpenCode Runtime | **不包含** | OpenCode 所需 Bun Runtime 当前不支持 LoongArch |
|
||||
| 原生 Canvas 图像解码 | **不包含** | npm 当前没有 `@napi-rs/canvas-linux-loong64-gnu` 包 |
|
||||
| Koffi musl binding | **不包含** | Koffi 3.1.4 只发布了 `linux_loong64` glibc binding |
|
||||
| 正式自动更新和发布索引 | **不支持** | 预览包不加入正式 CI、安装包矩阵或更新索引 |
|
||||
|
||||
在真机验证完成前,不应把“已打包”功能描述为正式可用。尤其是窗口显示、GPU、
|
||||
音频、系统托盘、桌面集成、PTY 交互和模型调用都必须在真实 LoongArch 桌面上
|
||||
重新验证。
|
||||
|
||||
## 与正式 GoodBuddy 隔离
|
||||
|
||||
预览版有意使用单独身份,避免覆盖正式安装:
|
||||
|
||||
- Debian 包名:`goodbuddy-loongarch-preview`
|
||||
- 产品名:`GoodBuddy LoongArch Preview`
|
||||
- 可执行文件:`goodbuddy-loongarch-preview`
|
||||
- 安装目录:`/opt/GoodBuddy-LoongArch-Preview`
|
||||
- Electron 应用身份与正式 GoodBuddy 不同,因此使用单独的用户数据目录
|
||||
|
||||
该流程不会修改 `.github/workflows/packages.yml`,也不会把 `loong64` 加入
|
||||
正式 release manifest 或下载索引。
|
||||
|
||||
## 构建前提
|
||||
|
||||
### 本地源码主机
|
||||
|
||||
- 已完成 `npm ci`
|
||||
- Node.js 和 npm 版本满足仓库要求
|
||||
- 可以成功执行 `npm run build:bundle`
|
||||
|
||||
### AMD64 交叉构建主机
|
||||
|
||||
- Linux AMD64
|
||||
- Docker
|
||||
- 建议至少 30 GiB 内存和 50 GiB 可用磁盘
|
||||
- 可访问 Ubuntu、npm、Electron headers 和社区 Electron Release
|
||||
|
||||
### Electron LoongArch shell
|
||||
|
||||
从
|
||||
[`darkyzhou/electron-loong64`](https://github.com/darkyzhou/electron-loong64)
|
||||
的 Releases 下载:
|
||||
|
||||
```text
|
||||
electron-v42.3.0-linux-loong64.zip
|
||||
```
|
||||
|
||||
构建脚本固定校验以下 SHA-256,摘要不匹配时必须停止:
|
||||
|
||||
```text
|
||||
92b0ca0c9c18ed90166918a4ac1970266c4fa967aee9277031b3b250b905526e
|
||||
```
|
||||
|
||||
这是社区产物,不是 Electron 官方发布的 Linux LoongArch 二进制。
|
||||
|
||||
## 第一步:生成架构无关输入
|
||||
|
||||
在 GoodBuddy 仓库根目录执行:
|
||||
|
||||
```sh
|
||||
npm ci
|
||||
npm run build:bundle
|
||||
node build/loongarch-cross/prepare-input.cjs
|
||||
```
|
||||
|
||||
默认输出:
|
||||
|
||||
```text
|
||||
dist/loongarch-cross/goodbuddy-loongarch-cross-input.tgz
|
||||
```
|
||||
|
||||
输入归档包含生产 `out`、锁文件、Skills、Continue Runtime、npm Runtime、
|
||||
图标和发布元数据。它不包含 Agent、Remote Runtime 或 OpenCode 二进制。
|
||||
|
||||
可通过 `--output` 指定其他输出位置:
|
||||
|
||||
```sh
|
||||
node build/loongarch-cross/prepare-input.cjs \
|
||||
--output /path/to/goodbuddy-loongarch-cross-input.tgz
|
||||
```
|
||||
|
||||
## 第二步:构建交叉工具链镜像
|
||||
|
||||
将 `build/loongarch-cross`、输入归档和 Electron zip 放到 AMD64 Docker
|
||||
主机,然后执行:
|
||||
|
||||
```sh
|
||||
docker build \
|
||||
--tag goodbuddy-loongarch-cross:ubuntu-26.04 \
|
||||
build/loongarch-cross
|
||||
```
|
||||
|
||||
工具链镜像包括:
|
||||
|
||||
- `loongarch64-linux-gnu-gcc` 和 G++
|
||||
- LoongArch glibc 开发 sysroot
|
||||
- QEMU user mode
|
||||
- CMake 和 Ninja
|
||||
- Rust `loongarch64-unknown-linux-gnu` target
|
||||
- Node.js、npm 和上游 node-gyp
|
||||
|
||||
可先运行基础探针:
|
||||
|
||||
```sh
|
||||
mkdir -p build/loongarch-cross/results
|
||||
docker run --rm \
|
||||
--volume "$PWD/build/loongarch-cross/results:/results" \
|
||||
goodbuddy-loongarch-cross:ubuntu-26.04
|
||||
```
|
||||
|
||||
探针必须通过 C、C++、CMake、Rust、NAPI-RS、QEMU、DEB、Koffi 和
|
||||
node-pty 检查。Canvas 和 Koffi musl 显示为 unavailable 是当前已知限制,
|
||||
不是 Debian 桌面主体 DEB 的构建失败。
|
||||
|
||||
## 第三步:生成预览 DEB
|
||||
|
||||
假设交叉构建主机上的文件布局如下:
|
||||
|
||||
```text
|
||||
input/goodbuddy-loongarch-cross-input.tgz
|
||||
electron/electron-v42.3.0-linux-loong64.zip
|
||||
preview-output/
|
||||
```
|
||||
|
||||
运行:
|
||||
|
||||
```sh
|
||||
docker run --rm \
|
||||
--entrypoint /usr/local/bin/goodbuddy-loongarch-preview-deb \
|
||||
--volume "$PWD/input:/input:ro" \
|
||||
--volume "$PWD/electron:/electron:ro" \
|
||||
--volume "$PWD/preview-output:/preview-output" \
|
||||
goodbuddy-loongarch-cross:ubuntu-26.04 \
|
||||
/input/goodbuddy-loongarch-cross-input.tgz \
|
||||
/electron/electron-v42.3.0-linux-loong64.zip \
|
||||
/preview-output
|
||||
```
|
||||
|
||||
每次构建创建一个 UTC 时间戳目录,包含:
|
||||
|
||||
```text
|
||||
GoodBuddy-<version>-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。
|
||||
+6
-5
@@ -4,11 +4,12 @@
|
||||
|
||||
正式站点地址:<https://mesalogo.github.io/goodbuddy/>
|
||||
|
||||
首页将 GoodBuddy 定位为“免注册、支持信创软硬件的一站式 AI 助手”,
|
||||
优先展示三大桌面系统与双架构下载入口、统一 Agent Runtime,以及知识库、
|
||||
魔法笔记、智能心跳、桌面上下文和远程消息通道等桌面助手能力。下载区位于
|
||||
主要功能说明之前,并明确列出统信 UOS、银河麒麟、海光、兆芯、鲲鹏和飞腾
|
||||
对应的 Linux x64 / arm64 包。页面不重复设置底部下载推广区。
|
||||
首页将 GoodBuddy 定位为“首个支持龙芯的开源桌面 AI 助理产品”和
|
||||
“免注册、支持信创软硬件的一站式 AI 助手”,优先展示三大桌面系统、正式
|
||||
x64 / arm64 下载入口、独立编译的龙芯 loong64 预览版、统一 Agent Runtime,
|
||||
以及知识库、魔法笔记、智能心跳、桌面上下文和远程消息通道等桌面助手能力。
|
||||
下载区位于主要功能说明之前,并明确列出统信 UOS、银河麒麟、海光、兆芯、
|
||||
鲲鹏、飞腾和龙芯。页面不重复设置底部下载推广区。
|
||||
英文页面位于 `en.html`,不展示信创适配文案,三个平台的下载按钮始终前往
|
||||
GitHub 最新正式 Release。
|
||||
首屏产品界面默认正面展示,在精确指针设备上使用克制的 3D 倾斜、
|
||||
|
||||
+22
-9
@@ -5,14 +5,14 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta
|
||||
name="description"
|
||||
content="GoodBuddy 是免注册、支持信创软硬件的一站式 AI 助手,以统一 Agent Runtime 连接直连模型、OpenCode、Continue 与 DeepSeek Harness。"
|
||||
content="GoodBuddy 是首个支持龙芯的开源桌面 AI 助理产品,也是免注册、支持信创软硬件的一站式 AI 助手;龙芯 LoongArch 当前提供独立编译预览版。"
|
||||
/>
|
||||
<link rel="canonical" href="https://mesalogo.github.io/goodbuddy/" />
|
||||
<link rel="alternate" hreflang="zh-CN" href="https://mesalogo.github.io/goodbuddy/" />
|
||||
<link rel="alternate" hreflang="en" href="https://mesalogo.github.io/goodbuddy/en.html" />
|
||||
<link rel="alternate" hreflang="x-default" href="https://mesalogo.github.io/goodbuddy/en.html" />
|
||||
<meta name="theme-color" content="#f6f8fb" />
|
||||
<title>GoodBuddy|免注册、支持信创软硬件的一站式 AI 助手</title>
|
||||
<title>GoodBuddy|首个支持龙芯的开源桌面 AI 助理产品</title>
|
||||
<link
|
||||
rel="icon"
|
||||
href="./assets/goodbuddy-light.png"
|
||||
@@ -116,7 +116,7 @@
|
||||
<div class="hero-copy">
|
||||
<div class="eyebrow">
|
||||
<span class="status-dot" aria-hidden="true"></span>
|
||||
Windows · macOS · Linux
|
||||
Windows · macOS · Linux · 龙芯预览版
|
||||
</div>
|
||||
<h1 id="hero-title">
|
||||
免注册,<br />
|
||||
@@ -124,7 +124,9 @@
|
||||
<span>一站式 AI 助手。</span>
|
||||
</h1>
|
||||
<p class="hero-lead">
|
||||
作为桌面助手与 AI 编程工具台,GoodBuddy 管理对话、知识、笔记与任务,
|
||||
GoodBuddy 是首个支持龙芯的开源桌面 AI 助理产品,龙芯 LoongArch
|
||||
当前以独立编译预览版提供。作为桌面助手与 AI 编程工具台,GoodBuddy
|
||||
管理对话、知识、笔记与任务,
|
||||
也通过独创的统一 Agent Runtime 接入直连模型、OpenCode、Continue 和 DeepSeek Harness。
|
||||
无需反复配置命令行,选择工具和项目即可开始。
|
||||
</p>
|
||||
@@ -137,6 +139,10 @@
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m5 10 3 3 7-7" /></svg>
|
||||
3 大桌面系统,x64 / arm64
|
||||
</li>
|
||||
<li>
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m5 10 3 3 7-7" /></svg>
|
||||
龙芯 LoongArch 独立编译预览版
|
||||
</li>
|
||||
<li>
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m5 10 3 3 7-7" /></svg>
|
||||
4 类 Agent Runtime 统一接入
|
||||
@@ -234,7 +240,7 @@
|
||||
<div class="section-inner proof-grid">
|
||||
<div><strong>4 类 Runtime</strong><span>多种 AI 编程路径</span></div>
|
||||
<div><strong>3 大系统</strong><span>Windows / macOS / Linux</span></div>
|
||||
<div><strong>2 种架构</strong><span>x64 / arm64</span></div>
|
||||
<div><strong>3 种架构</strong><span>x64 / arm64 / loong64 预览</span></div>
|
||||
<div><strong>1 个入口</strong><span>选择、配置、运行、审计</span></div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -347,14 +353,21 @@
|
||||
<aside class="domestic-support" aria-labelledby="domestic-support-title">
|
||||
<div>
|
||||
<p class="kicker">信创软硬件支持</p>
|
||||
<h3 id="domestic-support-title">统信 UOS、银河麒麟,覆盖国产 x64 与 ARM64</h3>
|
||||
<p>使用对应的 Linux x64 或 arm64 安装包。</p>
|
||||
<h3 id="domestic-support-title">首个支持龙芯的开源桌面 AI 助理产品</h3>
|
||||
<p>
|
||||
正式下载覆盖国产 x64 与 ARM64;龙芯 LoongArch 使用独立编译的
|
||||
loong64 预览版。
|
||||
</p>
|
||||
</div>
|
||||
<div class="domestic-support__items">
|
||||
<div><span>国产系统</span><strong>统信 UOS · 银河麒麟</strong></div>
|
||||
<div>
|
||||
<span>国产 CPU</span>
|
||||
<strong>海光 · 兆芯(x64)<br />鲲鹏 · 飞腾(ARM64)</strong>
|
||||
<strong>
|
||||
海光 · 兆芯(x64)<br />
|
||||
鲲鹏 · 飞腾(ARM64)<br />
|
||||
龙芯(loong64 预览版)
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -512,7 +525,7 @@
|
||||
</span>
|
||||
<span>GoodBuddy</span>
|
||||
</a>
|
||||
<p>免注册、支持信创软硬件的一站式 AI 助手</p>
|
||||
<p>首个支持龙芯的开源桌面 AI 助理产品</p>
|
||||
<div class="footer-links">
|
||||
<a href="#download">下载</a>
|
||||
<a href="#features">Agent Runtime</a>
|
||||
|
||||
@@ -250,6 +250,8 @@ for (const breakpoint of ["1199px", "959px", "719px"]) {
|
||||
const requiredCopy = [
|
||||
"免注册",
|
||||
"支持信创软硬件的一站式 AI 助手",
|
||||
"首个支持龙芯的开源桌面 AI 助理产品",
|
||||
"龙芯 LoongArch 独立编译预览版",
|
||||
"桌面助手",
|
||||
"AI 编程工具台",
|
||||
"Windows、macOS、Linux",
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import {
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync
|
||||
} from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
interface LoongArchInputBuilderModule {
|
||||
inputPaths: string[]
|
||||
parseArguments(arguments_: string[]): { output: string }
|
||||
verifyInputs(projectRoot?: string): void
|
||||
}
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const inputBuilder = require(
|
||||
'../build/loongarch-cross/prepare-input.cjs'
|
||||
) as LoongArchInputBuilderModule
|
||||
const previewScript = readFileSync(
|
||||
join(
|
||||
process.cwd(),
|
||||
'build',
|
||||
'loongarch-cross',
|
||||
'preview-deb.sh'
|
||||
),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
let temporaryDirectories: string[] = []
|
||||
|
||||
afterEach(() => {
|
||||
for (const directory of temporaryDirectories) {
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
}
|
||||
temporaryDirectories = []
|
||||
})
|
||||
|
||||
describe('LoongArch cross-build input', () => {
|
||||
it('supports an explicit output path and rejects unknown arguments', () => {
|
||||
const output = join(tmpdir(), 'goodbuddy-loongarch-input.tgz')
|
||||
expect(
|
||||
inputBuilder.parseArguments(['--output', output]).output
|
||||
).toBe(output)
|
||||
expect(() =>
|
||||
inputBuilder.parseArguments(['--unknown'])
|
||||
).toThrow('未知参数')
|
||||
expect(() =>
|
||||
inputBuilder.parseArguments(['--output'])
|
||||
).toThrow('--output 缺少值')
|
||||
})
|
||||
|
||||
it('requires every architecture-neutral packaging input', () => {
|
||||
const root = mkdtempSync(
|
||||
join(tmpdir(), 'goodbuddy-loongarch-input-test-')
|
||||
)
|
||||
temporaryDirectories.push(root)
|
||||
|
||||
for (const relativePath of inputBuilder.inputPaths) {
|
||||
const absolutePath = join(root, relativePath)
|
||||
mkdirSync(dirname(absolutePath), { recursive: true })
|
||||
writeFileSync(absolutePath, 'input')
|
||||
}
|
||||
|
||||
expect(() => inputBuilder.verifyInputs(root)).not.toThrow()
|
||||
|
||||
const missingPath = inputBuilder.inputPaths.at(-1)
|
||||
expect(missingPath).toBeDefined()
|
||||
rmSync(join(root, missingPath!), { force: true })
|
||||
expect(() => inputBuilder.verifyInputs(root)).toThrow(
|
||||
`龙芯交叉构建输入缺失:${missingPath}`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('LoongArch preview package contract', () => {
|
||||
it('keeps the preview isolated and verifies target native code', () => {
|
||||
expect(previewScript).toContain(
|
||||
'Package: goodbuddy-loongarch-preview'
|
||||
)
|
||||
expect(previewScript).toContain('Architecture: loong64')
|
||||
expect(previewScript).toContain(
|
||||
'goodbuddy-loongarch-preview'
|
||||
)
|
||||
expect(previewScript).toContain(
|
||||
'@koromix/koffi-linux-loong64'
|
||||
)
|
||||
expect(previewScript).toContain(
|
||||
'assert_loongarch_elf "${node_pty_binding}"'
|
||||
)
|
||||
expect(previewScript).toContain(
|
||||
'assert_loongarch_elf "${koffi_binding}"'
|
||||
)
|
||||
expect(previewScript).toContain('libpulse0')
|
||||
expect(previewScript).toContain('"bundledAgent": false')
|
||||
expect(previewScript).toContain(
|
||||
'"bundledRemoteRuntime": false'
|
||||
)
|
||||
expect(previewScript).toContain(
|
||||
'"bundledOpenCode": false'
|
||||
)
|
||||
expect(previewScript).toContain('"nativeCanvas": false')
|
||||
expect(previewScript).not.toContain('--no-sandbox')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user