Compare commits
18
Commits
3d8f4c45fd
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37ebcafe96
|
||
|
|
eef54a61cc
|
||
|
|
703bf076e2
|
||
|
|
5bbe489b56
|
||
|
|
4e4e1c4ffd
|
||
|
|
f28dd4e655
|
||
|
|
bf7ad90a6c
|
||
|
|
22694712f4
|
||
|
|
86047a49a2
|
||
|
|
38205f3958
|
||
|
|
a755ee8691
|
||
|
|
e57e845a3b
|
||
|
|
c901908eab
|
||
|
|
2c0b5286a7
|
||
|
|
59ac403063
|
||
|
|
6c7a87994e
|
||
|
|
3bd94c483d
|
||
|
|
8fb7d22591
|
No files matched your search
@@ -0,0 +1,206 @@
|
|||||||
|
name: Prepare RAM CI environment
|
||||||
|
description: Configure RAM-backed CI directories and Nexus package proxies
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Configure RAM CI environment
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
set +x
|
||||||
|
|
||||||
|
require_value() {
|
||||||
|
local name="$1"
|
||||||
|
local value="$2"
|
||||||
|
|
||||||
|
if [ -z "$value" ]; then
|
||||||
|
echo "ERROR: required CI value is empty: $name"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
require_value "NEXUS_CARGO_URL" "$NEXUS_CARGO_URL"
|
||||||
|
require_value "NEXUS_MAVEN_URL" "$NEXUS_MAVEN_URL"
|
||||||
|
require_value "NEXUS_NPM_URL" "$NEXUS_NPM_URL"
|
||||||
|
require_value "NEXUS_NODEJS_URL" "$NEXUS_NODEJS_URL"
|
||||||
|
|
||||||
|
normalize_url() {
|
||||||
|
local value="$1"
|
||||||
|
|
||||||
|
value="${value//$'\r'/}"
|
||||||
|
value="${value//$'\n'/}"
|
||||||
|
|
||||||
|
case "$value" in
|
||||||
|
\"*\")
|
||||||
|
value="${value#\"}"
|
||||||
|
value="${value%\"}"
|
||||||
|
;;
|
||||||
|
\'*\')
|
||||||
|
value="${value#\'}"
|
||||||
|
value="${value%\'}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
printf '%s' "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
NEXUS_CARGO_URL="$(normalize_url "$NEXUS_CARGO_URL")"
|
||||||
|
NEXUS_MAVEN_URL="$(normalize_url "$NEXUS_MAVEN_URL")"
|
||||||
|
NEXUS_NPM_URL="$(normalize_url "$NEXUS_NPM_URL")"
|
||||||
|
NEXUS_NODEJS_URL="$(normalize_url "$NEXUS_NODEJS_URL")"
|
||||||
|
|
||||||
|
NEXUS_CARGO_URL="${NEXUS_CARGO_URL%/}/"
|
||||||
|
NEXUS_MAVEN_URL="${NEXUS_MAVEN_URL%/}/"
|
||||||
|
NEXUS_NPM_URL="${NEXUS_NPM_URL%/}/"
|
||||||
|
NEXUS_NODEJS_URL="${NEXUS_NODEJS_URL%/}/"
|
||||||
|
|
||||||
|
# Resolve the image's stable toolchain before HOME/CARGO_HOME move to RAM.
|
||||||
|
# Keep toolchains in the image; only registry data and build output are ephemeral.
|
||||||
|
RUSTUP_HOME="${RUSTUP_HOME:-$HOME/.rustup}"
|
||||||
|
export RUSTUP_HOME
|
||||||
|
RUST_BIN="$(dirname "$(rustup which --toolchain stable rustc)")"
|
||||||
|
test -x "$RUST_BIN/cargo"
|
||||||
|
|
||||||
|
CI_ROOT="$GITHUB_WORKSPACE/.ci"
|
||||||
|
|
||||||
|
CI_HOME="$CI_ROOT/home"
|
||||||
|
CI_TMP="$CI_ROOT/tmp"
|
||||||
|
CI_BIN="$CI_ROOT/bin"
|
||||||
|
|
||||||
|
NPM_CONFIG_USERCONFIG="$CI_ROOT/npmrc"
|
||||||
|
NPM_CACHE="$CI_ROOT/npm-cache"
|
||||||
|
|
||||||
|
COREPACK_HOME="$CI_ROOT/corepack"
|
||||||
|
PNPM_HOME="$CI_ROOT/pnpm-home"
|
||||||
|
|
||||||
|
GRADLE_USER_HOME="$CI_ROOT/gradle"
|
||||||
|
|
||||||
|
CARGO_HOME="$CI_ROOT/cargo"
|
||||||
|
CARGO_TARGET_DIR="$CI_ROOT/cargo-target"
|
||||||
|
|
||||||
|
XDG_CACHE_HOME="$CI_ROOT/xdg-cache"
|
||||||
|
XDG_CONFIG_HOME="$CI_ROOT/xdg-config"
|
||||||
|
XDG_DATA_HOME="$CI_ROOT/xdg-data"
|
||||||
|
|
||||||
|
mkdir -p \
|
||||||
|
"$CI_HOME" \
|
||||||
|
"$CI_TMP" \
|
||||||
|
"$CI_BIN" \
|
||||||
|
"$NPM_CACHE" \
|
||||||
|
"$COREPACK_HOME" \
|
||||||
|
"$PNPM_HOME" \
|
||||||
|
"$GRADLE_USER_HOME/init.d" \
|
||||||
|
"$CARGO_HOME" \
|
||||||
|
"$CARGO_TARGET_DIR" \
|
||||||
|
"$XDG_CACHE_HOME" \
|
||||||
|
"$XDG_CONFIG_HOME" \
|
||||||
|
"$XDG_DATA_HOME"
|
||||||
|
|
||||||
|
chmod 700 \
|
||||||
|
"$CI_HOME" \
|
||||||
|
"$GRADLE_USER_HOME" \
|
||||||
|
"$CARGO_HOME"
|
||||||
|
|
||||||
|
cat > "$NPM_CONFIG_USERCONFIG" <<EOF
|
||||||
|
registry=$NEXUS_NPM_URL
|
||||||
|
cache=$NPM_CACHE
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod 600 "$NPM_CONFIG_USERCONFIG"
|
||||||
|
|
||||||
|
{
|
||||||
|
printf '%s\n' '[source.crates-io]'
|
||||||
|
printf '%s\n' 'replace-with = "cargo-proxy"'
|
||||||
|
printf '\n'
|
||||||
|
printf '%s\n' '[source.cargo-proxy]'
|
||||||
|
printf 'registry = "sparse+%s"\n' "$NEXUS_CARGO_URL"
|
||||||
|
} > "$CARGO_HOME/config.toml"
|
||||||
|
|
||||||
|
chmod 600 "$CARGO_HOME/config.toml"
|
||||||
|
|
||||||
|
if [ "$(wc -l < "$CARGO_HOME/config.toml")" -ne 5 ]; then
|
||||||
|
echo "ERROR: generated Cargo config has unexpected line count"
|
||||||
|
nl -ba "$CARGO_HOME/config.toml"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "$GRADLE_USER_HOME/init.d/00-nexus.init.gradle" <<'EOF'
|
||||||
|
def nexusUrl = System.getenv("NEXUS_MAVEN_URL")
|
||||||
|
|
||||||
|
if (nexusUrl == null || nexusUrl.isBlank()) {
|
||||||
|
throw new GradleException(
|
||||||
|
"NEXUS_MAVEN_URL is not configured"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def configureNexusRepository = { repositories ->
|
||||||
|
repositories.maven {
|
||||||
|
name = "CI_NEXUS"
|
||||||
|
url = uri(nexusUrl)
|
||||||
|
allowInsecureProtocol =
|
||||||
|
nexusUrl.startsWith("http://")
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gradle.beforeSettings { settings ->
|
||||||
|
configureNexusRepository(
|
||||||
|
settings.pluginManagement.repositories
|
||||||
|
)
|
||||||
|
|
||||||
|
configureNexusRepository(
|
||||||
|
settings.dependencyResolutionManagement.repositories
|
||||||
|
)
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod 600 \
|
||||||
|
"$GRADLE_USER_HOME/init.d/00-nexus.init.gradle"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "CI_ROOT=$CI_ROOT"
|
||||||
|
echo "CI_BIN=$CI_BIN"
|
||||||
|
|
||||||
|
echo "HOME=$CI_HOME"
|
||||||
|
echo "TMPDIR=$CI_TMP"
|
||||||
|
|
||||||
|
echo "XDG_CACHE_HOME=$XDG_CACHE_HOME"
|
||||||
|
echo "XDG_CONFIG_HOME=$XDG_CONFIG_HOME"
|
||||||
|
echo "XDG_DATA_HOME=$XDG_DATA_HOME"
|
||||||
|
|
||||||
|
echo "NPM_CONFIG_USERCONFIG=$NPM_CONFIG_USERCONFIG"
|
||||||
|
echo "NPM_CONFIG_CACHE=$NPM_CACHE"
|
||||||
|
echo "npm_config_cache=$NPM_CACHE"
|
||||||
|
|
||||||
|
echo "COREPACK_HOME=$COREPACK_HOME"
|
||||||
|
echo "COREPACK_NPM_REGISTRY=$NEXUS_NPM_URL"
|
||||||
|
|
||||||
|
echo "PNPM_HOME=$PNPM_HOME"
|
||||||
|
echo "PNPM_CONFIG_REGISTRY=$NEXUS_NPM_URL"
|
||||||
|
|
||||||
|
echo "GRADLE_USER_HOME=$GRADLE_USER_HOME"
|
||||||
|
|
||||||
|
echo "CARGO_HOME=$CARGO_HOME"
|
||||||
|
echo "CARGO_TARGET_DIR=$CARGO_TARGET_DIR"
|
||||||
|
echo "RUSTUP_HOME=$RUSTUP_HOME"
|
||||||
|
echo "RUSTUP_TOOLCHAIN=stable"
|
||||||
|
|
||||||
|
echo "NVM_NODEJS_ORG_MIRROR=$NEXUS_NODEJS_URL"
|
||||||
|
|
||||||
|
echo "NEXUS_CARGO_URL=$NEXUS_CARGO_URL"
|
||||||
|
echo "NEXUS_MAVEN_URL=$NEXUS_MAVEN_URL"
|
||||||
|
echo "NEXUS_NPM_URL=$NEXUS_NPM_URL"
|
||||||
|
echo "NEXUS_NODEJS_URL=$NEXUS_NODEJS_URL"
|
||||||
|
} >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "$RUST_BIN"
|
||||||
|
echo "$CI_BIN"
|
||||||
|
# cargo install places renderer WASM tools here (for example wasm-bindgen).
|
||||||
|
echo "$CARGO_HOME/bin"
|
||||||
|
echo "$PNPM_HOME"
|
||||||
|
} >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
echo "CI package environment prepared."
|
||||||
+191
-533
@@ -2,622 +2,280 @@ name: Build Fluxer Desktop
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
NODE_VERSION: "24"
|
NODE_VERSION: "26"
|
||||||
PNPM_VERSION: "10.29.3"
|
|
||||||
|
|
||||||
BUILD_CHANNEL: "stable"
|
BUILD_CHANNEL: "stable"
|
||||||
NODE_ENV: "production"
|
NODE_ENV: "production"
|
||||||
FLUXER_DESKTOP_PRODUCTION: "true"
|
FLUXER_DESKTOP_PRODUCTION: "true"
|
||||||
|
# Bundle the renderer; Gitea releases do not provide Fluxer module manifests.
|
||||||
|
FLUXER_MODULES: "0"
|
||||||
|
FLUXER_OFFLINE: "0"
|
||||||
VERSION: "1.0.${{ gitea.run_number }}"
|
VERSION: "1.0.${{ gitea.run_number }}"
|
||||||
|
BUILD_VERSION: "1.0.${{ gitea.run_number }}"
|
||||||
PUBLIC_BUILD_VERSION: "1.0.${{ gitea.run_number }}"
|
PUBLIC_BUILD_VERSION: "1.0.${{ gitea.run_number }}"
|
||||||
PUBLIC_RELEASE_CHANNEL: "stable"
|
PUBLIC_RELEASE_CHANNEL: "stable"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
windows-x64:
|
shared-renderer:
|
||||||
name: Windows x64
|
name: Shared renderer and upstream revision
|
||||||
|
runs-on: ci-linux-builder
|
||||||
runs-on: rustdesk-windows
|
outputs:
|
||||||
|
sha: ${{ steps.source.outputs.sha }}
|
||||||
|
env:
|
||||||
|
NEXUS_CARGO_URL: ${{ secrets.NEXUS_CARGO_URL }}
|
||||||
|
NEXUS_MAVEN_URL: ${{ secrets.NEXUS_MAVEN_URL }}
|
||||||
|
NEXUS_NPM_URL: ${{ secrets.NEXUS_NPM_URL }}
|
||||||
|
NEXUS_NODEJS_URL: ${{ secrets.NEXUS_NODEJS_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Clone Fluxer
|
- name: Checkout CI repository
|
||||||
shell: powershell
|
uses: actions/checkout@v4
|
||||||
run: |
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
|
|
||||||
if (Test-Path "fluxer-src") {
|
|
||||||
Remove-Item "fluxer-src" -Recurse -Force
|
|
||||||
}
|
|
||||||
|
|
||||||
git clone `
|
|
||||||
--depth 1 `
|
|
||||||
--branch main `
|
|
||||||
https://github.com/fluxerapp/fluxer.git `
|
|
||||||
fluxer-src
|
|
||||||
|
|
||||||
if (-not (Test-Path "fluxer-src\fluxer_desktop\src\common\Constants.ts")) {
|
|
||||||
throw "Fluxer source tree is invalid: Constants.ts not found"
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "Fluxer revision:"
|
|
||||||
git -C fluxer-src rev-parse HEAD
|
|
||||||
|
|
||||||
Write-Host "Desktop directory:"
|
|
||||||
Get-ChildItem "fluxer-src\fluxer_desktop"
|
|
||||||
|
|
||||||
|
- name: Prepare RAM CI environment
|
||||||
|
uses: ./.gitea/actions/prepare-ram-ci
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "24"
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
|
||||||
|
- name: Clone current upstream main
|
||||||
|
id: source
|
||||||
|
shell: bash
|
||||||
|
run: node scripts/prepare-upstream.mjs
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Apply GSCORD patches
|
||||||
|
shell: bash
|
||||||
|
run: node scripts/apply-patches.mjs fluxer-src
|
||||||
|
|
||||||
|
- name: Setup pnpm from upstream packageManager
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
package_json_file: fluxer-src/package.json
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Prepare renderer WASM toolchain
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
rustup update stable
|
||||||
|
rustup target add wasm32-unknown-unknown --toolchain stable
|
||||||
|
rustc --version
|
||||||
|
clang --version
|
||||||
|
llvm-ar --version
|
||||||
|
|
||||||
|
- name: Install dependencies including build tools
|
||||||
|
shell: bash
|
||||||
|
working-directory: fluxer-src
|
||||||
|
run: pnpm install --frozen-lockfile --prod=false
|
||||||
|
|
||||||
|
- name: Check GSCORD behavior
|
||||||
|
shell: bash
|
||||||
|
run: node --test scripts/customizations.test.mjs
|
||||||
|
|
||||||
|
- name: Build shared renderer
|
||||||
|
shell: bash
|
||||||
|
working-directory: fluxer-src
|
||||||
|
run: pnpm --filter fluxer_desktop build --shared-assets
|
||||||
|
|
||||||
|
- name: Archive renderer
|
||||||
|
shell: bash
|
||||||
|
run: tar -C fluxer-src/fluxer_desktop/dist -czf renderer.tar.gz renderer
|
||||||
|
|
||||||
|
- name: Upload renderer
|
||||||
|
uses: actions/upload-artifact@v4.6.2
|
||||||
|
with:
|
||||||
|
name: gscord-shared-renderer
|
||||||
|
path: renderer.tar.gz
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
windows-x64:
|
||||||
|
name: Windows x64
|
||||||
|
needs: shared-renderer
|
||||||
|
runs-on: rustdesk-windows
|
||||||
|
env:
|
||||||
|
UPSTREAM_SHA: ${{ needs.shared-renderer.outputs.sha }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout CI repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
|
||||||
|
- name: Clone selected upstream revision
|
||||||
shell: powershell
|
shell: powershell
|
||||||
run: |
|
run: |
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
node scripts/prepare-upstream.mjs
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Upstream checkout failed" }
|
||||||
|
|
||||||
corepack enable
|
- name: Apply GSCORD patches
|
||||||
corepack prepare pnpm@10.29.3 --activate
|
shell: powershell
|
||||||
|
run: |
|
||||||
node --version
|
$ErrorActionPreference = "Stop"
|
||||||
pnpm --version
|
node scripts/apply-patches.mjs fluxer-src
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Patch application failed" }
|
||||||
|
|
||||||
|
- name: Setup pnpm from upstream packageManager
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
package_json_file: fluxer-src/package.json
|
||||||
|
run_install: false
|
||||||
|
|
||||||
- name: Setup Rust
|
- name: Setup Rust
|
||||||
shell: bash
|
shell: powershell
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
$ErrorActionPreference = "Stop"
|
||||||
|
rustup update stable
|
||||||
RUST_VERSION="1.98.1"
|
if ($LASTEXITCODE -ne 0) { throw "Rust update failed" }
|
||||||
|
rustup default stable
|
||||||
rm -f /tmp/rustup-init
|
if ($LASTEXITCODE -ne 0) { throw "Rust selection failed" }
|
||||||
|
rustup target add x86_64-pc-windows-msvc
|
||||||
curl \
|
if ($LASTEXITCODE -ne 0) { throw "Rust target installation failed" }
|
||||||
--fail \
|
|
||||||
--location \
|
|
||||||
--retry 5 \
|
|
||||||
--retry-delay 3 \
|
|
||||||
--retry-all-errors \
|
|
||||||
--connect-timeout 20 \
|
|
||||||
https://static.rust-lang.org/rustup/dist/x86_64-unknown-linux-gnu/rustup-init \
|
|
||||||
-o /tmp/rustup-init
|
|
||||||
|
|
||||||
chmod +x /tmp/rustup-init
|
|
||||||
|
|
||||||
/tmp/rustup-init \
|
|
||||||
-y \
|
|
||||||
--profile minimal \
|
|
||||||
--default-toolchain "${RUST_VERSION}" \
|
|
||||||
--default-host x86_64-unknown-linux-gnu \
|
|
||||||
--no-modify-path
|
|
||||||
|
|
||||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
|
||||||
|
|
||||||
"$HOME/.cargo/bin/rustc" --version
|
|
||||||
"$HOME/.cargo/bin/cargo" --version
|
|
||||||
"$HOME/.cargo/bin/rustup" --version
|
|
||||||
|
|
||||||
|
|
||||||
- name: Show build tools
|
- name: Show build tools
|
||||||
shell: powershell
|
shell: powershell
|
||||||
run: |
|
run: |
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
foreach ($Tool in @("node", "pnpm", "rustc", "cargo", "cmake", "ninja", "clang", "nasm")) {
|
||||||
|
& $Tool --version
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Build tool check failed: $Tool" }
|
||||||
|
}
|
||||||
|
|
||||||
Write-Host "Node:"
|
- name: Install dependencies including build tools
|
||||||
node --version
|
shell: powershell
|
||||||
|
working-directory: fluxer-src
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
pnpm install --frozen-lockfile --prod=false
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Dependency installation failed" }
|
||||||
|
|
||||||
Write-Host "pnpm:"
|
- name: Download renderer
|
||||||
pnpm --version
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: gscord-shared-renderer
|
||||||
|
path: shared-renderer
|
||||||
|
|
||||||
Write-Host "Rust:"
|
- name: Extract renderer
|
||||||
rustc --version
|
|
||||||
|
|
||||||
Write-Host "Cargo:"
|
|
||||||
cargo --version
|
|
||||||
|
|
||||||
Write-Host "CMake:"
|
|
||||||
cmake --version
|
|
||||||
|
|
||||||
Write-Host "Ninja:"
|
|
||||||
ninja --version
|
|
||||||
|
|
||||||
Write-Host "Clang:"
|
|
||||||
clang --version
|
|
||||||
|
|
||||||
Write-Host "NASM:"
|
|
||||||
nasm --version
|
|
||||||
|
|
||||||
|
|
||||||
- name: Configure self-hosted server
|
|
||||||
shell: powershell
|
shell: powershell
|
||||||
run: |
|
run: |
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
New-Item -ItemType Directory -Force fluxer-src/fluxer_desktop/dist | Out-Null
|
||||||
$Path = "fluxer-src\fluxer_desktop\src\common\Constants.ts"
|
tar -xzf shared-renderer/renderer.tar.gz -C fluxer-src/fluxer_desktop/dist
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Renderer extraction failed" }
|
||||||
if (-not (Test-Path $Path)) {
|
|
||||||
throw "Constants.ts not found at $Path"
|
|
||||||
}
|
|
||||||
|
|
||||||
$Content = Get-Content $Path -Raw
|
|
||||||
|
|
||||||
$Content = $Content -replace `
|
|
||||||
"export const STABLE_APP_URL = '[^']+';", `
|
|
||||||
"export const STABLE_APP_URL = 'https://ds.gandc.ru';"
|
|
||||||
|
|
||||||
$Content = $Content -replace `
|
|
||||||
"export const CANARY_APP_URL = '[^']+';", `
|
|
||||||
"export const CANARY_APP_URL = 'https://ds.gandc.ru';"
|
|
||||||
|
|
||||||
Set-Content `
|
|
||||||
-Path $Path `
|
|
||||||
-Value $Content `
|
|
||||||
-NoNewline `
|
|
||||||
-Encoding utf8
|
|
||||||
|
|
||||||
Write-Host "Result:"
|
|
||||||
Get-Content $Path
|
|
||||||
|
|
||||||
$Result = Get-Content $Path -Raw
|
|
||||||
|
|
||||||
if ($Result -notmatch "STABLE_APP_URL = 'https://ds\.gandc\.ru'") {
|
|
||||||
throw "STABLE_APP_URL was not configured"
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($Result -notmatch "CANARY_APP_URL = 'https://ds\.gandc\.ru'") {
|
|
||||||
throw "CANARY_APP_URL was not configured"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
|
|
||||||
Set-Location fluxer-src
|
|
||||||
|
|
||||||
pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
|
|
||||||
- name: Build Fluxer Desktop
|
- name: Build Fluxer Desktop
|
||||||
shell: powershell
|
shell: powershell
|
||||||
|
working-directory: fluxer-src
|
||||||
env:
|
env:
|
||||||
ELECTRON_ARCH: x64
|
ELECTRON_ARCH: x64
|
||||||
run: |
|
run: |
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
pnpm --filter fluxer_desktop build --use-shared-renderer
|
||||||
Set-Location fluxer-src
|
if ($LASTEXITCODE -ne 0) { throw "Desktop build failed" }
|
||||||
|
|
||||||
pnpm --filter fluxer_desktop build
|
|
||||||
|
|
||||||
|
|
||||||
- name: Package Windows portable
|
- name: Package Windows portable
|
||||||
shell: powershell
|
shell: powershell
|
||||||
|
working-directory: fluxer-src/fluxer_desktop
|
||||||
env:
|
env:
|
||||||
ELECTRON_ARCH: x64
|
ELECTRON_ARCH: x64
|
||||||
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
||||||
run: |
|
run: |
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
pnpm exec electron-builder --config electron-builder.config.cjs --win portable --x64 --publish never
|
||||||
Set-Location fluxer-src\fluxer_desktop
|
if ($LASTEXITCODE -ne 0) { throw "Windows packaging failed" }
|
||||||
|
|
||||||
pnpm exec electron-builder `
|
|
||||||
--config electron-builder.config.cjs `
|
|
||||||
--win portable `
|
|
||||||
--x64 `
|
|
||||||
--publish never
|
|
||||||
|
|
||||||
|
|
||||||
- name: Show Windows artifacts
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
|
|
||||||
Get-ChildItem `
|
|
||||||
-Path "fluxer-src\fluxer_desktop\dist-electron" `
|
|
||||||
-Recurse |
|
|
||||||
Select-Object FullName, Length
|
|
||||||
|
|
||||||
|
|
||||||
- name: Upload Windows artifact
|
- name: Upload Windows artifact
|
||||||
uses: actions/upload-artifact@v4.6.2
|
uses: actions/upload-artifact@v4.6.2
|
||||||
with:
|
with:
|
||||||
name: fluxer-windows-x64
|
name: fluxer-windows-x64
|
||||||
path: |
|
path: fluxer-src/fluxer_desktop/dist-electron/*.exe
|
||||||
fluxer-src/fluxer_desktop/dist-electron/*.exe
|
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
|
||||||
linux-x64:
|
linux-x64:
|
||||||
name: Linux x64
|
name: Linux x64
|
||||||
|
needs: shared-renderer
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ci-linux-builder
|
||||||
|
env:
|
||||||
|
UPSTREAM_SHA: ${{ needs.shared-renderer.outputs.sha }}
|
||||||
|
NEXUS_CARGO_URL: ${{ secrets.NEXUS_CARGO_URL }}
|
||||||
|
NEXUS_MAVEN_URL: ${{ secrets.NEXUS_MAVEN_URL }}
|
||||||
|
NEXUS_NPM_URL: ${{ secrets.NEXUS_NPM_URL }}
|
||||||
|
NEXUS_NODEJS_URL: ${{ secrets.NEXUS_NODEJS_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Clone Fluxer
|
- name: Checkout CI repository
|
||||||
shell: bash
|
uses: actions/checkout@v4
|
||||||
run: |
|
|
||||||
set -euxo pipefail
|
|
||||||
|
|
||||||
rm -rf fluxer-src
|
- name: Prepare RAM CI environment
|
||||||
|
uses: ./.gitea/actions/prepare-ram-ci
|
||||||
git clone \
|
|
||||||
--depth 1 \
|
|
||||||
--branch main \
|
|
||||||
https://github.com/fluxerapp/fluxer.git \
|
|
||||||
fluxer-src
|
|
||||||
|
|
||||||
test -f \
|
|
||||||
fluxer-src/fluxer_desktop/src/common/Constants.ts
|
|
||||||
|
|
||||||
echo "Fluxer revision:"
|
|
||||||
git -C fluxer-src rev-parse HEAD
|
|
||||||
|
|
||||||
echo "Desktop directory:"
|
|
||||||
ls -la fluxer-src/fluxer_desktop
|
|
||||||
|
|
||||||
|
|
||||||
- name: Install Linux build dependencies
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euxo pipefail
|
|
||||||
|
|
||||||
apt-get update
|
|
||||||
|
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
|
||||||
build-essential \
|
|
||||||
ca-certificates \
|
|
||||||
clang \
|
|
||||||
cmake \
|
|
||||||
curl \
|
|
||||||
desktop-file-utils \
|
|
||||||
dpkg-dev \
|
|
||||||
fakeroot \
|
|
||||||
ffmpeg \
|
|
||||||
git \
|
|
||||||
libasound2 \
|
|
||||||
libaom-dev \
|
|
||||||
libavcodec-dev \
|
|
||||||
libavfilter-dev \
|
|
||||||
libavformat-dev \
|
|
||||||
libavutil-dev \
|
|
||||||
libcbor-dev \
|
|
||||||
libdbus-1-dev \
|
|
||||||
libdav1d-dev \
|
|
||||||
libde265-dev \
|
|
||||||
libfido2-dev \
|
|
||||||
libgbm1 \
|
|
||||||
libgtk-3-0 \
|
|
||||||
liblcms2-dev \
|
|
||||||
libnotify4 \
|
|
||||||
libnss3 \
|
|
||||||
libpipewire-0.3-dev \
|
|
||||||
libpulse-dev \
|
|
||||||
libsecret-1-0 \
|
|
||||||
libssl-dev \
|
|
||||||
libswresample-dev \
|
|
||||||
libswscale-dev \
|
|
||||||
libudev-dev \
|
|
||||||
libvips-dev \
|
|
||||||
libwayland-dev \
|
|
||||||
libwebp-dev \
|
|
||||||
libx11-xcb1 \
|
|
||||||
libxcb-dri3-0 \
|
|
||||||
libxcomposite1 \
|
|
||||||
libxdamage1 \
|
|
||||||
libxkbcommon-dev \
|
|
||||||
libxkbcommon0 \
|
|
||||||
libxrandr2 \
|
|
||||||
libxshmfence1 \
|
|
||||||
libxss1 \
|
|
||||||
libxtst6 \
|
|
||||||
llvm \
|
|
||||||
meson \
|
|
||||||
nasm \
|
|
||||||
ninja-build \
|
|
||||||
pkg-config \
|
|
||||||
python3 \
|
|
||||||
python3-pip \
|
|
||||||
rpm \
|
|
||||||
unzip \
|
|
||||||
xdg-utils \
|
|
||||||
xz-utils \
|
|
||||||
yasm \
|
|
||||||
zlib1g-dev \
|
|
||||||
zstd
|
|
||||||
|
|
||||||
- name: Install modern libfido2
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euxo pipefail
|
|
||||||
|
|
||||||
rm -rf /tmp/libfido2
|
|
||||||
|
|
||||||
git clone \
|
|
||||||
--depth 1 \
|
|
||||||
--branch 1.17.0 \
|
|
||||||
https://github.com/Yubico/libfido2.git \
|
|
||||||
/tmp/libfido2
|
|
||||||
|
|
||||||
cmake \
|
|
||||||
-S /tmp/libfido2 \
|
|
||||||
-B /tmp/libfido2/build \
|
|
||||||
-DCMAKE_BUILD_TYPE=Release \
|
|
||||||
-DCMAKE_INSTALL_PREFIX=/usr/local \
|
|
||||||
-DCMAKE_INSTALL_LIBDIR=lib/x86_64-linux-gnu \
|
|
||||||
-DBUILD_EXAMPLES=OFF \
|
|
||||||
-DBUILD_MANPAGES=OFF \
|
|
||||||
-DBUILD_TOOLS=OFF
|
|
||||||
|
|
||||||
cmake \
|
|
||||||
--build /tmp/libfido2/build \
|
|
||||||
--parallel "$(nproc)"
|
|
||||||
|
|
||||||
cmake \
|
|
||||||
--install /tmp/libfido2/build
|
|
||||||
|
|
||||||
ldconfig
|
|
||||||
|
|
||||||
echo "=== libfido2 ==="
|
|
||||||
|
|
||||||
PKG_CONFIG_PATH="/usr/local/lib/x86_64-linux-gnu/pkgconfig:/usr/local/lib/pkgconfig:/usr/lib/x86_64-linux-gnu/pkgconfig" \
|
|
||||||
pkg-config --modversion libfido2
|
|
||||||
|
|
||||||
test -f /usr/local/include/fido/es384.h
|
|
||||||
|
|
||||||
ls -la /usr/local/include/fido/
|
|
||||||
|
|
||||||
echo "=== libraries ==="
|
|
||||||
|
|
||||||
find /usr/local/lib \
|
|
||||||
-name 'libfido2*' \
|
|
||||||
-print
|
|
||||||
|
|
||||||
- name: Install modern PipeWire
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euxo pipefail
|
|
||||||
|
|
||||||
PIPEWIRE_VERSION="1.0.5"
|
|
||||||
|
|
||||||
rm -rf /tmp/pipewire
|
|
||||||
|
|
||||||
git clone \
|
|
||||||
--depth 1 \
|
|
||||||
--branch "${PIPEWIRE_VERSION}" \
|
|
||||||
https://github.com/PipeWire/pipewire.git \
|
|
||||||
/tmp/pipewire
|
|
||||||
|
|
||||||
meson setup \
|
|
||||||
/tmp/pipewire/build \
|
|
||||||
/tmp/pipewire \
|
|
||||||
--prefix=/usr/local \
|
|
||||||
--libdir=lib/x86_64-linux-gnu \
|
|
||||||
--buildtype=release \
|
|
||||||
-Ddocs=disabled \
|
|
||||||
-Dman=disabled \
|
|
||||||
-Dexamples=disabled \
|
|
||||||
-Dtests=disabled \
|
|
||||||
-Dinstalled_tests=disabled \
|
|
||||||
-Dsession-managers=[] \
|
|
||||||
-Dgstreamer=disabled \
|
|
||||||
-Dsystemd=disabled \
|
|
||||||
-Dselinux=disabled \
|
|
||||||
-Dpipewire-jack=disabled \
|
|
||||||
-Dpipewire-v4l2=disabled \
|
|
||||||
-Dbluez5=disabled \
|
|
||||||
-Djack=disabled \
|
|
||||||
-Dv4l2=disabled \
|
|
||||||
-Dlibcamera=disabled \
|
|
||||||
-Dvulkan=disabled \
|
|
||||||
-Dsdl2=disabled \
|
|
||||||
-Dlibmysofa=disabled \
|
|
||||||
-Droc=disabled \
|
|
||||||
-Davahi=disabled \
|
|
||||||
-Decho-cancel-webrtc=disabled \
|
|
||||||
-Dlibusb=disabled \
|
|
||||||
-Draop=disabled \
|
|
||||||
-Dlv2=disabled \
|
|
||||||
-Dlibcanberra=disabled \
|
|
||||||
-Dlibffado=disabled
|
|
||||||
|
|
||||||
meson compile \
|
|
||||||
-C /tmp/pipewire/build \
|
|
||||||
-j "$(nproc)"
|
|
||||||
|
|
||||||
meson install \
|
|
||||||
-C /tmp/pipewire/build
|
|
||||||
|
|
||||||
ldconfig
|
|
||||||
|
|
||||||
export PKG_CONFIG_PATH="/usr/local/lib/x86_64-linux-gnu/pkgconfig:/usr/local/lib/pkgconfig:/usr/lib/x86_64-linux-gnu/pkgconfig"
|
|
||||||
|
|
||||||
echo "=== PipeWire ==="
|
|
||||||
|
|
||||||
pkg-config \
|
|
||||||
--modversion \
|
|
||||||
libpipewire-0.3
|
|
||||||
|
|
||||||
pkg-config \
|
|
||||||
--modversion \
|
|
||||||
libspa-0.2
|
|
||||||
|
|
||||||
echo "=== PipeWire CFLAGS ==="
|
|
||||||
|
|
||||||
pkg-config \
|
|
||||||
--cflags \
|
|
||||||
libpipewire-0.3
|
|
||||||
|
|
||||||
pkg-config \
|
|
||||||
--cflags \
|
|
||||||
libspa-0.2
|
|
||||||
|
|
||||||
echo "=== Checking headers ==="
|
|
||||||
|
|
||||||
grep -R \
|
|
||||||
"spa_meta_region_is_valid" \
|
|
||||||
/usr/local/include/spa-0.2/spa \
|
|
||||||
| head
|
|
||||||
|
|
||||||
echo "=== PipeWire libraries ==="
|
|
||||||
|
|
||||||
find /usr/local/lib \
|
|
||||||
\( -name 'libpipewire-0.3.so*' -o -name 'libspa*.so*' \) \
|
|
||||||
-print
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "24"
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
|
||||||
|
- name: Clone selected upstream revision
|
||||||
- name: Setup pnpm
|
|
||||||
shell: bash
|
shell: bash
|
||||||
|
run: node scripts/prepare-upstream.mjs
|
||||||
|
|
||||||
|
- name: Apply GSCORD patches
|
||||||
|
shell: bash
|
||||||
|
run: node scripts/apply-patches.mjs fluxer-src
|
||||||
|
|
||||||
|
- name: Setup pnpm from upstream packageManager
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
package_json_file: fluxer-src/package.json
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Verify builder image and update Rust
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
PKG_CONFIG_PATH: /usr/local/lib/x86_64-linux-gnu/pkgconfig:/usr/local/lib/pkgconfig:/usr/lib/x86_64-linux-gnu/pkgconfig
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euo pipefail
|
||||||
|
rustup update stable
|
||||||
corepack enable
|
|
||||||
corepack prepare pnpm@10.29.3 --activate
|
|
||||||
|
|
||||||
node --version
|
node --version
|
||||||
pnpm --version
|
pnpm --version
|
||||||
|
rustc --version
|
||||||
|
cargo --version
|
||||||
|
cmake --version
|
||||||
|
meson --version
|
||||||
|
ninja --version
|
||||||
|
clang --version
|
||||||
|
pkg-config --atleast-version=1.17.0 libfido2
|
||||||
|
pkg-config --atleast-version=1.0.5 libpipewire-0.3
|
||||||
|
pkg-config --modversion libfido2 libpipewire-0.3 libspa-0.2
|
||||||
|
test -f /usr/local/include/fido/es384.h
|
||||||
|
|
||||||
|
- name: Install dependencies including build tools
|
||||||
|
shell: bash
|
||||||
|
working-directory: fluxer-src
|
||||||
|
run: pnpm install --frozen-lockfile --prod=false
|
||||||
|
|
||||||
- name: Setup Rust
|
- name: Download renderer
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: gscord-shared-renderer
|
||||||
|
path: shared-renderer
|
||||||
|
|
||||||
|
- name: Extract renderer
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euo pipefail
|
||||||
|
mkdir -p fluxer-src/fluxer_desktop/dist
|
||||||
curl \
|
tar -xzf shared-renderer/renderer.tar.gz -C fluxer-src/fluxer_desktop/dist
|
||||||
--proto '=https' \
|
|
||||||
--tlsv1.2 \
|
|
||||||
-sSf \
|
|
||||||
https://sh.rustup.rs |
|
|
||||||
sh -s -- -y --profile minimal
|
|
||||||
|
|
||||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
|
||||||
|
|
||||||
"$HOME/.cargo/bin/rustup" update stable
|
|
||||||
"$HOME/.cargo/bin/rustup" default stable
|
|
||||||
|
|
||||||
"$HOME/.cargo/bin/rustc" --version
|
|
||||||
"$HOME/.cargo/bin/cargo" --version
|
|
||||||
|
|
||||||
|
|
||||||
- name: Configure self-hosted server
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euxo pipefail
|
|
||||||
|
|
||||||
python3 <<'PY'
|
|
||||||
from pathlib import Path
|
|
||||||
import re
|
|
||||||
|
|
||||||
path = Path(
|
|
||||||
"fluxer-src/fluxer_desktop/src/common/Constants.ts"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not path.is_file():
|
|
||||||
raise SystemExit(
|
|
||||||
f"Constants.ts not found: {path}"
|
|
||||||
)
|
|
||||||
|
|
||||||
data = path.read_text(
|
|
||||||
encoding="utf-8"
|
|
||||||
)
|
|
||||||
|
|
||||||
data, stable_count = re.subn(
|
|
||||||
r"export const STABLE_APP_URL = '[^']+';",
|
|
||||||
"export const STABLE_APP_URL = 'https://ds.gandc.ru';",
|
|
||||||
data,
|
|
||||||
count=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
data, canary_count = re.subn(
|
|
||||||
r"export const CANARY_APP_URL = '[^']+';",
|
|
||||||
"export const CANARY_APP_URL = 'https://ds.gandc.ru';",
|
|
||||||
data,
|
|
||||||
count=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
if stable_count != 1:
|
|
||||||
raise SystemExit(
|
|
||||||
"Unable to patch STABLE_APP_URL"
|
|
||||||
)
|
|
||||||
|
|
||||||
if canary_count != 1:
|
|
||||||
raise SystemExit(
|
|
||||||
"Unable to patch CANARY_APP_URL"
|
|
||||||
)
|
|
||||||
|
|
||||||
path.write_text(
|
|
||||||
data,
|
|
||||||
encoding="utf-8"
|
|
||||||
)
|
|
||||||
|
|
||||||
result = path.read_text(
|
|
||||||
encoding="utf-8"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
"export const STABLE_APP_URL = "
|
|
||||||
"'https://ds.gandc.ru';"
|
|
||||||
not in result
|
|
||||||
):
|
|
||||||
raise SystemExit(
|
|
||||||
"STABLE_APP_URL verification failed"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
"export const CANARY_APP_URL = "
|
|
||||||
"'https://ds.gandc.ru';"
|
|
||||||
not in result
|
|
||||||
):
|
|
||||||
raise SystemExit(
|
|
||||||
"CANARY_APP_URL verification failed"
|
|
||||||
)
|
|
||||||
|
|
||||||
print(result)
|
|
||||||
PY
|
|
||||||
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euxo pipefail
|
|
||||||
|
|
||||||
cd fluxer-src
|
|
||||||
|
|
||||||
pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
|
|
||||||
- name: Build Fluxer Desktop
|
- name: Build Fluxer Desktop
|
||||||
shell: bash
|
shell: bash
|
||||||
|
working-directory: fluxer-src
|
||||||
env:
|
env:
|
||||||
ELECTRON_ARCH: x64
|
ELECTRON_ARCH: x64
|
||||||
PKG_CONFIG_PATH: /usr/local/lib/x86_64-linux-gnu/pkgconfig:/usr/local/lib/pkgconfig:/usr/lib/x86_64-linux-gnu/pkgconfig
|
PKG_CONFIG_PATH: /usr/local/lib/x86_64-linux-gnu/pkgconfig:/usr/local/lib/pkgconfig:/usr/lib/x86_64-linux-gnu/pkgconfig
|
||||||
LD_LIBRARY_PATH: /usr/local/lib/x86_64-linux-gnu:/usr/local/lib
|
LD_LIBRARY_PATH: /usr/local/lib/x86_64-linux-gnu:/usr/local/lib
|
||||||
run: |
|
run: pnpm --filter fluxer_desktop build --use-shared-renderer
|
||||||
set -euxo pipefail
|
|
||||||
|
|
||||||
echo "libfido2 version:"
|
|
||||||
pkg-config --modversion libfido2
|
|
||||||
|
|
||||||
echo "libfido2 include flags:"
|
|
||||||
pkg-config --cflags libfido2
|
|
||||||
|
|
||||||
echo "Checking ES384 header:"
|
|
||||||
test -f /usr/local/include/fido/es384.h
|
|
||||||
|
|
||||||
cd fluxer-src
|
|
||||||
|
|
||||||
pnpm --filter fluxer_desktop build
|
|
||||||
|
|
||||||
- name: Inspect Linux native libraries
|
- name: Inspect Linux native libraries
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# GSCORD Desktop CI
|
||||||
|
|
||||||
|
Workflow собирает Fluxer Desktop для Windows x64 (portable EXE) и Linux x64
|
||||||
|
(AppImage, DEB). Патчи находятся в `patches/`, порядок — в `patches/series`.
|
||||||
|
`node scripts/apply-patches.mjs fluxer-src` сначала проверяет всю серию через
|
||||||
|
`git apply --check`, затем применяет её через `git apply`.
|
||||||
|
|
||||||
|
## Изменения
|
||||||
|
|
||||||
|
- `0001-self-hosted-instance.patch`: `STABLE_APP_URL`, `CANARY_APP_URL`, desktop
|
||||||
|
homepage и адреса миграции — `https://ds.gandc.ru`. Новый встроенный renderer
|
||||||
|
начинает discovery с этого сервера; явный launch override сохраняет приоритет.
|
||||||
|
- `0002-sso-navigation.patch`: точный origin `https://sso.gandc.ru` и возврат на
|
||||||
|
`https://ds.gandc.ru` разрешены для navigation/redirect. Проверка доверия к IPC
|
||||||
|
и разрешениям остаётся upstream: внешняя SSO-страница не становится локальным
|
||||||
|
доверенным renderer.
|
||||||
|
- `0003-webrtc-livekit.patch`: `relay → all` в main world для конструктора
|
||||||
|
`RTCPeerConnection` и `setConfiguration()`. Сохранён существовавший workaround
|
||||||
|
LiveKit offer-with-join через `CompressionStream`/`WebSocket`.
|
||||||
|
- `0004-gitea-updates.patch`: проверка последнего релиза через Gitea API,
|
||||||
|
ссылки на реальные assets с учётом платформы и архитектуры. Windows и Linux,
|
||||||
|
включая AppImage, используют ручное скачивание обновления. Для DEB не
|
||||||
|
предлагаются отсутствующие RPM/tar.gz assets. Пустой список ведёт на releases.
|
||||||
|
|
||||||
|
Проверок, валящих сборку при новых дефолтных Fluxer hostnames, нет. Неизменённые
|
||||||
|
upstream-адреса могут оставаться в исходниках. Если upstream изменит строки,
|
||||||
|
затрагиваемые патчами, обычный конфликт `git apply` остановит сборку.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. `shared-renderer` получает текущий upstream `main`, выдаёт его SHA для обоих
|
||||||
|
платформенных заданий, применяет патчи и собирает общий renderer на Linux.
|
||||||
|
2. Windows и Linux скачивают тот же SHA, применяют ту же серию и собирают native
|
||||||
|
modules + Electron с `--use-shared-renderer`.
|
||||||
|
3. Сохраняются существующие runner labels и Linux Nexus/RAM-настройки.
|
||||||
|
|
||||||
|
Node.js — 26, как в текущем upstream build workflow. Версия pnpm читается из
|
||||||
|
upstream `package.json` (`11.27.0` на проверенном commit), а зависимости ставятся
|
||||||
|
с `--frozen-lockfile --prod=false`, чтобы `NODE_ENV=production` не исключал
|
||||||
|
инструменты сборки. Rust stable обновляется перед сборкой; для renderer нужен
|
||||||
|
`wasm32-unknown-unknown`, clang с WASM backend и llvm-ar. Путь `$CARGO_HOME/bin`
|
||||||
|
добавлен в RAM action, поскольку upstream устанавливает туда `wasm-bindgen`.
|
||||||
|
|
||||||
|
`FLUXER_MODULES=0` сохраняет полный локальный renderer: Gitea releases не являются
|
||||||
|
сервером Fluxer module manifests. `FLUXER_OFFLINE=0` сохраняет обычный сетевой
|
||||||
|
клиент. Теперь web-renderer обновляется вместе с desktop-сборкой, а не просто
|
||||||
|
подгружается как страница сервера. Совместимость renderer с API вашего сервера
|
||||||
|
нужно проверить запуском готового клиента.
|
||||||
|
|
||||||
|
В Windows добавлен checkout CI-репозитория и явные проверки exit code внешних
|
||||||
|
команд. `.gitattributes` сохраняет LF в патчах на Windows.
|
||||||
|
|
||||||
|
## Локальная проверка
|
||||||
|
|
||||||
|
Из корня этого репозитория с Node.js 24+ и Git:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
node scripts/prepare-upstream.mjs
|
||||||
|
node scripts/apply-patches.mjs fluxer-src --check
|
||||||
|
node scripts/apply-patches.mjs fluxer-src
|
||||||
|
node --test scripts/customizations.test.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
`prepare-upstream.mjs` требует отсутствия `fluxer-src`, чтобы не затереть чужие
|
||||||
|
изменения. Для проверки конкретной версии передайте `UPSTREAM_SHA` с полным SHA.
|
||||||
|
Для тестов против другой директории можно задать `FLUXER_SOURCE`.
|
||||||
|
|
||||||
|
## Проверено 11 октября 2026
|
||||||
|
|
||||||
|
- Исходный GSCORD commit: `703bf076e2fe3a253cb6cf6591d64f6ddad54343`.
|
||||||
|
- Upstream `main`: `575a5adbeff0b46390fb08901c7166649255204c`.
|
||||||
|
- Все четыре патча прошли `git apply --check --whitespace=error-all` и реальное
|
||||||
|
применение на чистом checkout этого upstream commit.
|
||||||
|
- 12 проверок: адреса/начальный сервер, SSO allowlist, ICE constructor и
|
||||||
|
setConfiguration, subclass/default behavior, LiveKit, Gitea assets/архитектуры,
|
||||||
|
download links и выбор manual updater.
|
||||||
|
- YAML workflow и composite action разобраны; Bash-блоки прошли `bash -n`.
|
||||||
|
- Workflow прошёл actionlint 1.7.12. Для этого GitHub-ориентированного валидатора
|
||||||
|
только в временной копии заменён `gitea.run_number` на `github.run_number` и
|
||||||
|
указаны два пользовательских runner label; shellcheck/pyflakes отключены.
|
||||||
|
- JavaScript helper scripts и изменённый TypeScript проверены на синтаксис.
|
||||||
|
|
||||||
|
Полная установка зависимостей, TypeScript typecheck, native-сборка, упаковка и
|
||||||
|
живые проверки SSO/звонков здесь не запускались. Их нужно выполнить на ваших
|
||||||
|
CI runner; успешное применение патчей не подтверждает совместимость серверного
|
||||||
|
API или наличие всех системных библиотек в builder image.
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
diff --git a/fluxer_desktop/package.json b/fluxer_desktop/package.json
|
||||||
|
index e1b4f22..dd0cc08 100644
|
||||||
|
--- a/fluxer_desktop/package.json
|
||||||
|
+++ b/fluxer_desktop/package.json
|
||||||
|
@@ -2,7 +2,7 @@
|
||||||
|
"name": "fluxer_desktop",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Instant messaging and VoIP application",
|
||||||
|
- "homepage": "https://fluxer.app",
|
||||||
|
+ "homepage": "https://ds.gandc.ru",
|
||||||
|
"author": "Fluxer Platform AB <support@fluxer.com>",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"private": true,
|
||||||
|
diff --git a/fluxer_desktop/src/common/Constants.ts b/fluxer_desktop/src/common/Constants.ts
|
||||||
|
index 04709f7..6fdcff1 100644
|
||||||
|
--- a/fluxer_desktop/src/common/Constants.ts
|
||||||
|
+++ b/fluxer_desktop/src/common/Constants.ts
|
||||||
|
@@ -14,8 +14,8 @@ export const CHANNEL_APP_PROTOCOLS: Record<BuildChannel, string> = {
|
||||||
|
development: 'fluxer-development',
|
||||||
|
};
|
||||||
|
export const APP_PROTOCOL = CHANNEL_APP_PROTOCOLS[BUILD_CHANNEL];
|
||||||
|
-export const STABLE_APP_URL = 'https://web.fluxer.app';
|
||||||
|
-export const CANARY_APP_URL = 'https://web.canary.fluxer.app';
|
||||||
|
+export const STABLE_APP_URL = 'https://ds.gandc.ru';
|
||||||
|
+export const CANARY_APP_URL = 'https://ds.gandc.ru';
|
||||||
|
const DEVELOPMENT_APP_URL = 'http://localhost:8088';
|
||||||
|
export const CHANNEL_APP_URLS: Record<BuildChannel, string> = {
|
||||||
|
stable: STABLE_APP_URL,
|
||||||
|
@@ -28,8 +28,8 @@ export const DOWNLOAD_PAGE_URLS: Record<BuildChannel, string> = {
|
||||||
|
canary: 'https://canary.fluxer.app/download',
|
||||||
|
development: 'http://localhost:8088/download',
|
||||||
|
};
|
||||||
|
-export const STABLE_MIGRATED_APP_ORIGIN = 'https://fluxer.com';
|
||||||
|
-export const CANARY_MIGRATED_APP_ORIGIN = 'https://canary.fluxer.com';
|
||||||
|
+export const STABLE_MIGRATED_APP_ORIGIN = 'https://ds.gandc.ru';
|
||||||
|
+export const CANARY_MIGRATED_APP_ORIGIN = 'https://ds.gandc.ru';
|
||||||
|
export const DESKTOP_APP_SCHEME = DESKTOP_LOCAL_APP_SCHEME;
|
||||||
|
export const DESKTOP_APP_HOST = DESKTOP_LOCAL_APP_HOST;
|
||||||
|
export const DESKTOP_APP_ORIGIN = DESKTOP_LOCAL_APP_ORIGIN;
|
||||||
|
diff --git a/fluxer_desktop/src/main/DesktopRuntimeDiscovery.ts b/fluxer_desktop/src/main/DesktopRuntimeDiscovery.ts
|
||||||
|
index 3b23773..ddcc993 100644
|
||||||
|
--- a/fluxer_desktop/src/main/DesktopRuntimeDiscovery.ts
|
||||||
|
+++ b/fluxer_desktop/src/main/DesktopRuntimeDiscovery.ts
|
||||||
|
@@ -1,5 +1,7 @@
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
+import {BUILD_CHANNEL} from '@electron/common/BuildChannel';
|
||||||
|
+import {CHANNEL_APP_URLS} from '@electron/common/Constants';
|
||||||
|
import {getLaunchInstanceEndpointOverride} from '@electron/common/DesktopConfig';
|
||||||
|
import {createChildLogger} from '@electron/common/Logger';
|
||||||
|
import {getDesktopAppStorage} from '@electron/main/DesktopAppStorage';
|
||||||
|
@@ -140,7 +142,7 @@ export async function restoreRememberedRuntimePlan(instanceKey: string): Promise
|
||||||
|
}
|
||||||
|
|
||||||
|
export function desktopRuntimeInitialInput(): string | null {
|
||||||
|
- return getLaunchInstanceEndpointOverride();
|
||||||
|
+ return getLaunchInstanceEndpointOverride() ?? CHANNEL_APP_URLS[BUILD_CHANNEL];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchRuntimePlan(
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
diff --git a/fluxer_desktop/src/main/Window.ts b/fluxer_desktop/src/main/Window.ts
|
||||||
|
index f537d89..b0a3454 100644
|
||||||
|
--- a/fluxer_desktop/src/main/Window.ts
|
||||||
|
+++ b/fluxer_desktop/src/main/Window.ts
|
||||||
|
@@ -104,13 +104,22 @@ function getSanitizedPath(rawUrl: string): string | null {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
+function isAllowedSsoNavigation(url: string): boolean {
|
||||||
|
+ try {
|
||||||
|
+ const origin = new URL(url).origin;
|
||||||
|
+ return origin === 'https://sso.gandc.ru' || origin === 'https://ds.gandc.ru';
|
||||||
|
+ } catch {
|
||||||
|
+ return false;
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
function preventUntrustedNavigation(event: Electron.Event, url: string, isMainFrame: boolean): void {
|
||||||
|
if (isLocalAppURL(url) && !isLocalAppRendererDocumentURL(url)) {
|
||||||
|
logger.warn('Blocked a navigation to a reserved local app path', {url, isMainFrame});
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
- if (isMainFrame && !isTrustedOrigin(url)) {
|
||||||
|
+ if (isMainFrame && !isTrustedOrigin(url) && !isAllowedSsoNavigation(url)) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -128,6 +137,9 @@ function attachChildWindowGuards(webContents: Electron.WebContents): void {
|
||||||
|
webContents.on('will-frame-navigate', (event) => {
|
||||||
|
preventUntrustedChildNavigation(event, event.url, event.isMainFrame);
|
||||||
|
});
|
||||||
|
+ webContents.on('will-redirect', (event, url, _isInPlace, isMainFrame) => {
|
||||||
|
+ preventUntrustedChildNavigation(event, url, isMainFrame);
|
||||||
|
+ });
|
||||||
|
webContents.setWindowOpenHandler(({url}) => {
|
||||||
|
openExternalDeduped(url).catch((error) => {
|
||||||
|
log.warn('Failed to open external URL from a popout window-open:', error);
|
||||||
|
@@ -1081,6 +1093,9 @@ export function createWindow(options: CreateWindowOptions = {}): BrowserWindow {
|
||||||
|
webContents.on('will-frame-navigate', (event) => {
|
||||||
|
preventUntrustedNavigation(event, event.url, event.isMainFrame);
|
||||||
|
});
|
||||||
|
+ webContents.on('will-redirect', (event, url, _isInPlace, isMainFrame) => {
|
||||||
|
+ preventUntrustedNavigation(event, url, isMainFrame);
|
||||||
|
+ });
|
||||||
|
webContents.on('did-create-window', (window, details) => {
|
||||||
|
if (
|
||||||
|
details.frameName === THEME_STUDIO_POPOUT_WINDOW_NAME &&
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
diff --git a/fluxer_desktop/src/preload/GscordCompatibility.ts b/fluxer_desktop/src/preload/GscordCompatibility.ts
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000..9085d5a
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/fluxer_desktop/src/preload/GscordCompatibility.ts
|
||||||
|
@@ -0,0 +1,127 @@
|
||||||
|
+// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
+
|
||||||
|
+// These functions are serialized by executeInMainWorld: keep them self-contained.
|
||||||
|
+export function installForceAllIceTransportPolicy(): void {
|
||||||
|
+ const NativeRTCPeerConnection = window.RTCPeerConnection;
|
||||||
|
+
|
||||||
|
+ if (typeof NativeRTCPeerConnection !== 'function') {
|
||||||
|
+ return;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ const nativeSetConfiguration = NativeRTCPeerConnection.prototype.setConfiguration;
|
||||||
|
+ NativeRTCPeerConnection.prototype.setConfiguration = function (configuration: RTCConfiguration): void {
|
||||||
|
+ const next = configuration?.iceTransportPolicy === 'relay'
|
||||||
|
+ ? {...configuration, iceTransportPolicy: 'all' as RTCIceTransportPolicy}
|
||||||
|
+ : configuration;
|
||||||
|
+ return nativeSetConfiguration.call(this, next);
|
||||||
|
+ };
|
||||||
|
+
|
||||||
|
+ window.RTCPeerConnection = new Proxy(NativeRTCPeerConnection, {
|
||||||
|
+ construct(Target, args, newTarget) {
|
||||||
|
+ const config = args[0] as RTCConfiguration | undefined;
|
||||||
|
+
|
||||||
|
+ if (config?.iceTransportPolicy === 'relay') {
|
||||||
|
+ args[0] = {
|
||||||
|
+ ...config,
|
||||||
|
+ iceTransportPolicy: 'all',
|
||||||
|
+ };
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ return Reflect.construct(Target, args, newTarget);
|
||||||
|
+ },
|
||||||
|
+ });
|
||||||
|
+
|
||||||
|
+ console.info('[Fluxer Desktop] WebRTC iceTransportPolicy forced to all');
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+export function installDisableLiveKitOfferWithJoin(): void {
|
||||||
|
+ const NativeWebSocket = window.WebSocket;
|
||||||
|
+ const compressionDescriptor = Object.getOwnPropertyDescriptor(
|
||||||
|
+ window,
|
||||||
|
+ 'CompressionStream',
|
||||||
|
+ );
|
||||||
|
+
|
||||||
|
+ let compressionSuppressed = false;
|
||||||
|
+
|
||||||
|
+ const suppressCompressionStream = (): void => {
|
||||||
|
+ if (compressionSuppressed) {
|
||||||
|
+ return;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ Object.defineProperty(window, 'CompressionStream', {
|
||||||
|
+ configurable: true,
|
||||||
|
+ writable: true,
|
||||||
|
+ value: undefined,
|
||||||
|
+ });
|
||||||
|
+
|
||||||
|
+ compressionSuppressed = true;
|
||||||
|
+ };
|
||||||
|
+
|
||||||
|
+ const restoreCompressionStream = (): void => {
|
||||||
|
+ if (!compressionSuppressed) {
|
||||||
|
+ return;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if (compressionDescriptor) {
|
||||||
|
+ Object.defineProperty(
|
||||||
|
+ window,
|
||||||
|
+ 'CompressionStream',
|
||||||
|
+ compressionDescriptor,
|
||||||
|
+ );
|
||||||
|
+ } else {
|
||||||
|
+ delete (window as typeof window & {
|
||||||
|
+ CompressionStream?: typeof CompressionStream;
|
||||||
|
+ }).CompressionStream;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ compressionSuppressed = false;
|
||||||
|
+ };
|
||||||
|
+
|
||||||
|
+ const isLiveKitRtcSocket = (value: string | URL): boolean => {
|
||||||
|
+ try {
|
||||||
|
+ const url = new URL(String(value), window.location.href);
|
||||||
|
+ return /\/rtc\/v1\/?$/.test(url.pathname);
|
||||||
|
+ } catch {
|
||||||
|
+ return false;
|
||||||
|
+ }
|
||||||
|
+ };
|
||||||
|
+
|
||||||
|
+ window.WebSocket = new Proxy(NativeWebSocket, {
|
||||||
|
+ construct(Target, args) {
|
||||||
|
+ const isLiveKitRtc = isLiveKitRtcSocket(
|
||||||
|
+ args[0] as string | URL,
|
||||||
|
+ );
|
||||||
|
+
|
||||||
|
+ if (isLiveKitRtc) {
|
||||||
|
+ restoreCompressionStream();
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ let socket: WebSocket;
|
||||||
|
+
|
||||||
|
+ try {
|
||||||
|
+ socket = Reflect.construct(Target, args, Target);
|
||||||
|
+ } catch (error) {
|
||||||
|
+ if (isLiveKitRtc) {
|
||||||
|
+ suppressCompressionStream();
|
||||||
|
+ }
|
||||||
|
+ throw error;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if (isLiveKitRtc) {
|
||||||
|
+ socket.addEventListener(
|
||||||
|
+ 'close',
|
||||||
|
+ () => suppressCompressionStream(),
|
||||||
|
+ {once: true},
|
||||||
|
+ );
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ return socket;
|
||||||
|
+ },
|
||||||
|
+ }) as typeof WebSocket;
|
||||||
|
+
|
||||||
|
+ suppressCompressionStream();
|
||||||
|
+
|
||||||
|
+ console.info(
|
||||||
|
+ '[Fluxer Desktop] LiveKit publisher offer-with-join disabled',
|
||||||
|
+ );
|
||||||
|
+}
|
||||||
|
diff --git a/fluxer_desktop/src/preload/index.ts b/fluxer_desktop/src/preload/index.ts
|
||||||
|
index 07ef677..12d4913 100644
|
||||||
|
--- a/fluxer_desktop/src/preload/index.ts
|
||||||
|
+++ b/fluxer_desktop/src/preload/index.ts
|
||||||
|
@@ -59,6 +59,7 @@ import type {
|
||||||
|
} from '@electron/common/Types';
|
||||||
|
import {createBrowserHandoffPreloadAPI} from '@electron/preload/BrowserHandoffPreload';
|
||||||
|
import {createDesktopStoragePreloadAPI} from '@electron/preload/DesktopStoragePreload';
|
||||||
|
+import {installDisableLiveKitOfferWithJoin, installForceAllIceTransportPolicy} from '@electron/preload/GscordCompatibility';
|
||||||
|
import {createFirstContentPaintSignal} from '@electron/preload/FirstContentPaint';
|
||||||
|
import {applyLegacyRawLocalStorage} from '@electron/preload/LegacyRawStorageSeed';
|
||||||
|
import {createLocalAppPreloadAPI} from '@electron/preload/LocalAppPreload';
|
||||||
|
@@ -86,6 +87,9 @@ import type {
|
||||||
|
} from '@simplewebauthn/browser';
|
||||||
|
import {contextBridge, ipcRenderer, webFrame} from 'electron';
|
||||||
|
|
||||||
|
+contextBridge.executeInMainWorld({func: installForceAllIceTransportPolicy});
|
||||||
|
+contextBridge.executeInMainWorld({func: installDisableLiveKitOfferWithJoin});
|
||||||
|
+
|
||||||
|
const ACCESSIBILITY_STORE_STORAGE_KEY = 'AccessibilityStore';
|
||||||
|
const ACTIVE_ALLOW_TRANSPARENCY_RENDERER_ARG = '--fluxer-active-allow-transparency=1';
|
||||||
|
const ACTIVE_USE_NATIVE_TITLEBAR_RENDERER_ARG = '--fluxer-active-use-native-titlebar=1';
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
diff --git a/fluxer_desktop/src/main/GscordRelease.ts b/fluxer_desktop/src/main/GscordRelease.ts
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000..54e6593
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/fluxer_desktop/src/main/GscordRelease.ts
|
||||||
|
@@ -0,0 +1,44 @@
|
||||||
|
+// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
+
|
||||||
|
+import type {ManualDesktopFormat} from '@electron/main/ShellDownloadFormats';
|
||||||
|
+import type {ManualLatestInfo} from '@electron/main/UpdaterDownloads';
|
||||||
|
+
|
||||||
|
+function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+export function parseGscordRelease(
|
||||||
|
+ payload: unknown,
|
||||||
|
+ platform: NodeJS.Platform = process.platform,
|
||||||
|
+ arch: NodeJS.Architecture = process.arch,
|
||||||
|
+): ManualLatestInfo {
|
||||||
|
+ if (!isRecord(payload) || typeof payload.tag_name !== 'string' || !payload.tag_name.trim()) {
|
||||||
|
+ throw new Error('GSCORD latest release response missing tag_name');
|
||||||
|
+ }
|
||||||
|
+ const version = payload.tag_name.trim().replace(/^v/, '');
|
||||||
|
+ if (!version) throw new Error('GSCORD latest release response missing version');
|
||||||
|
+ const files: ManualLatestInfo['files'] = {};
|
||||||
|
+ const arm = arch === 'arm64';
|
||||||
|
+ const patterns: Array<[ManualDesktopFormat, RegExp]> = platform === 'linux'
|
||||||
|
+ ? [
|
||||||
|
+ ['appimage', arm ? /-linux-arm64\.AppImage$/i : /-linux-x86_64\.AppImage$/i],
|
||||||
|
+ ['deb', arm ? /-linux-arm64\.deb$/i : /-linux-amd64\.deb$/i],
|
||||||
|
+ ['rpm', arm ? /-linux-aarch64\.rpm$/i : /-linux-x86_64\.rpm$/i],
|
||||||
|
+ ['tar_gz', arm ? /-linux-arm64\.tar\.gz$/i : /-linux-x64\.tar\.gz$/i],
|
||||||
|
+ ]
|
||||||
|
+ : platform === 'win32'
|
||||||
|
+ ? [['setup', arm ? /-win-arm64\.exe$/i : /-win-x64\.exe$/i]]
|
||||||
|
+ : [];
|
||||||
|
+ for (const asset of Array.isArray(payload.assets) ? payload.assets : []) {
|
||||||
|
+ if (!isRecord(asset) || typeof asset.name !== 'string' ||
|
||||||
|
+ typeof asset.browser_download_url !== 'string' || !asset.browser_download_url.trim()) continue;
|
||||||
|
+ const format = patterns.find(([, pattern]) => pattern.test(asset.name as string))?.[0];
|
||||||
|
+ if (format) files[format] = {url: asset.browser_download_url, sha256: null};
|
||||||
|
+ }
|
||||||
|
+ return {
|
||||||
|
+ version,
|
||||||
|
+ pubDate: typeof payload.published_at === 'string' ? payload.published_at
|
||||||
|
+ : typeof payload.created_at === 'string' ? payload.created_at : null,
|
||||||
|
+ files,
|
||||||
|
+ };
|
||||||
|
+}
|
||||||
|
diff --git a/fluxer_desktop/src/main/ShellUpdateCapability.ts b/fluxer_desktop/src/main/ShellUpdateCapability.ts
|
||||||
|
index ce31659..465e2e4 100644
|
||||||
|
--- a/fluxer_desktop/src/main/ShellUpdateCapability.ts
|
||||||
|
+++ b/fluxer_desktop/src/main/ShellUpdateCapability.ts
|
||||||
|
@@ -40,18 +40,13 @@ export function decideShellUpdatePlan(runtime: ShellUpdateRuntime): ShellUpdateP
|
||||||
|
if (runtime.flatpak) {
|
||||||
|
return {capability: ShellUpdateCapability.MANAGED_PACKAGE};
|
||||||
|
}
|
||||||
|
- if (runtime.portable) {
|
||||||
|
+ // GSCORD publishes portable EXE/AppImage/DEB assets, not upstream updater feeds.
|
||||||
|
+ if (runtime.portable || runtime.platform === 'win32' || runtime.platform === 'linux') {
|
||||||
|
return {capability: ShellUpdateCapability.MANUAL_DOWNLOAD, reason: 'platform'};
|
||||||
|
}
|
||||||
|
- if (runtime.platform === 'win32') {
|
||||||
|
- return {capability: ShellUpdateCapability.SELF_UPDATE, updater: 'velopack'};
|
||||||
|
- }
|
||||||
|
if (runtime.platform === 'darwin') {
|
||||||
|
return {capability: ShellUpdateCapability.SELF_UPDATE, updater: 'electron'};
|
||||||
|
}
|
||||||
|
- if (runtime.platform === 'linux' && runtime.appImage != null) {
|
||||||
|
- return {capability: ShellUpdateCapability.SELF_UPDATE, updater: 'appimage', target: runtime.appImage};
|
||||||
|
- }
|
||||||
|
return {capability: ShellUpdateCapability.MANUAL_DOWNLOAD, reason: 'platform'};
|
||||||
|
}
|
||||||
|
|
||||||
|
diff --git a/fluxer_desktop/src/main/Updater.ts b/fluxer_desktop/src/main/Updater.ts
|
||||||
|
index 4403ed7..dc0420b 100644
|
||||||
|
--- a/fluxer_desktop/src/main/Updater.ts
|
||||||
|
+++ b/fluxer_desktop/src/main/Updater.ts
|
||||||
|
@@ -1,13 +1,12 @@
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
import {checkDesktopUpdateNow} from '@electron/main/DesktopUpdateGate';
|
||||||
|
-import {MANUAL_DESKTOP_FORMATS, type ManualDesktopFormat} from '@electron/main/ShellDownloadFormats';
|
||||||
|
+import {parseGscordRelease} from '@electron/main/GscordRelease';
|
||||||
|
import {resolveShellUpdatePlan, ShellUpdateCapability} from '@electron/main/ShellUpdateCapability';
|
||||||
|
import {
|
||||||
|
DOWNLOAD_PAGE_URL,
|
||||||
|
getManualDownloadOptions,
|
||||||
|
getManualDownloadUrl,
|
||||||
|
- type ManualLatestFile,
|
||||||
|
type ManualLatestInfo,
|
||||||
|
UPDATE_BASE_URL,
|
||||||
|
type UpdaterDownloadOption,
|
||||||
|
@@ -48,9 +47,6 @@ function send(win: BrowserWindow | null, event: UpdaterEvent) {
|
||||||
|
win?.webContents.send('updater-event', event);
|
||||||
|
}
|
||||||
|
|
||||||
|
-function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
- return value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||||
|
-}
|
||||||
|
|
||||||
|
function getErrorMessage(error: unknown): string {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
@@ -89,23 +85,6 @@ function compareVersions(a: string, b: string): number {
|
||||||
|
return aPre < bPre ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
-function parseManualLatestFiles(value: unknown): Partial<Record<ManualDesktopFormat, ManualLatestFile>> {
|
||||||
|
- if (!isRecord(value)) {
|
||||||
|
- return {};
|
||||||
|
- }
|
||||||
|
- const files: Partial<Record<ManualDesktopFormat, ManualLatestFile>> = {};
|
||||||
|
- for (const format of MANUAL_DESKTOP_FORMATS) {
|
||||||
|
- const entry = value[format];
|
||||||
|
- if (!isRecord(entry) || typeof entry.url !== 'string' || entry.url.trim().length === 0) {
|
||||||
|
- continue;
|
||||||
|
- }
|
||||||
|
- files[format] = {
|
||||||
|
- url: entry.url,
|
||||||
|
- sha256: typeof entry.sha256 === 'string' ? entry.sha256 : null,
|
||||||
|
- };
|
||||||
|
- }
|
||||||
|
- return files;
|
||||||
|
-}
|
||||||
|
|
||||||
|
async function fetchManualLatest(options: {forceRefresh?: boolean} = {}): Promise<ManualLatestInfo> {
|
||||||
|
const now = Date.now();
|
||||||
|
@@ -123,15 +102,7 @@ async function fetchManualLatest(options: {forceRefresh?: boolean} = {}): Promis
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Latest version request failed: ${response.status}`);
|
||||||
|
}
|
||||||
|
- const payload = (await response.json()) as {version?: unknown; pub_date?: unknown; files?: unknown};
|
||||||
|
- if (typeof payload.version !== 'string' || payload.version.length === 0) {
|
||||||
|
- throw new Error('Latest version response missing version string');
|
||||||
|
- }
|
||||||
|
- const info: ManualLatestInfo = {
|
||||||
|
- version: payload.version,
|
||||||
|
- pubDate: typeof payload.pub_date === 'string' ? payload.pub_date : null,
|
||||||
|
- files: parseManualLatestFiles(payload.files),
|
||||||
|
- };
|
||||||
|
+ const info = parseGscordRelease(await response.json());
|
||||||
|
manualLatestCache = {at: now, info};
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
diff --git a/fluxer_desktop/src/main/UpdaterDownloads.ts b/fluxer_desktop/src/main/UpdaterDownloads.ts
|
||||||
|
index f63685b..c82ca74 100644
|
||||||
|
--- a/fluxer_desktop/src/main/UpdaterDownloads.ts
|
||||||
|
+++ b/fluxer_desktop/src/main/UpdaterDownloads.ts
|
||||||
|
@@ -1,11 +1,8 @@
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
-import {BUILD_CHANNEL} from '@electron/common/BuildChannel';
|
||||||
|
-import {DOWNLOAD_PAGE_URLS} from '@electron/common/Constants';
|
||||||
|
import {DESKTOP_ARTIFACT_PRODUCT_NAME} from '@electron/common/DesktopIdentity';
|
||||||
|
import {
|
||||||
|
DESKTOP_DOWNLOAD_ARCH,
|
||||||
|
- getUpdateBaseUrl,
|
||||||
|
isLinuxManualDesktopFormat,
|
||||||
|
LINUX_MANUAL_ARCH_TOKENS,
|
||||||
|
LINUX_MANUAL_FORMAT_EXTENSIONS,
|
||||||
|
@@ -21,8 +18,8 @@ export type UpdaterDownloadOption = {
|
||||||
|
sha256?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
-export const UPDATE_BASE_URL = getUpdateBaseUrl();
|
||||||
|
-export const DOWNLOAD_PAGE_URL = DOWNLOAD_PAGE_URLS[BUILD_CHANNEL];
|
||||||
|
+export const DOWNLOAD_PAGE_URL = 'https://git.gandc.ru/gscord/gscord-desktop/releases';
|
||||||
|
+export const UPDATE_BASE_URL = 'https://git.gandc.ru/api/v1/repos/gscord/gscord-desktop/releases';
|
||||||
|
|
||||||
|
export type ManualLatestFile = {url: string; sha256: string | null};
|
||||||
|
|
||||||
|
@@ -53,7 +50,9 @@ const LINUX_MANUAL_FORMAT_LABELS: Record<LinuxManualDesktopFormat, string> = {
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildManualVersionDownloadUrl(version: string, format: ManualDesktopFormat): string {
|
||||||
|
- return `${UPDATE_BASE_URL}/${version}/${format}`;
|
||||||
|
+ void version;
|
||||||
|
+ void format;
|
||||||
|
+ return DOWNLOAD_PAGE_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getArtifactProductName(): string {
|
||||||
|
@@ -72,12 +71,13 @@ export function getManualDownloadOptions(info: ManualLatestInfo): Array<UpdaterD
|
||||||
|
}
|
||||||
|
return getManualDownloadFormatPreference()
|
||||||
|
.filter(isLinuxManualDesktopFormat)
|
||||||
|
+ .filter((format) => info.files[format] != null)
|
||||||
|
.map((format) => {
|
||||||
|
const file = info.files[format];
|
||||||
|
return {
|
||||||
|
format,
|
||||||
|
label: LINUX_MANUAL_FORMAT_LABELS[format],
|
||||||
|
- url: buildManualVersionDownloadUrl(info.version, format),
|
||||||
|
+ url: file?.url ?? buildManualVersionDownloadUrl(info.version, format),
|
||||||
|
suggestedName: getManualUpdateSuggestedName(format, info.version),
|
||||||
|
sha256: file?.sha256 ?? null,
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
0001-self-hosted-instance.patch
|
||||||
|
0002-sso-navigation.patch
|
||||||
|
0003-webrtc-livekit.patch
|
||||||
|
0004-gitea-updates.patch
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import {spawnSync} from 'node:child_process';
|
||||||
|
import {mkdtempSync, readFileSync, rmSync, writeFileSync} from 'node:fs';
|
||||||
|
import {tmpdir} from 'node:os';
|
||||||
|
import {dirname, join, resolve} from 'node:path';
|
||||||
|
import {fileURLToPath} from 'node:url';
|
||||||
|
|
||||||
|
const patchDir = resolve(dirname(fileURLToPath(import.meta.url)), '../patches');
|
||||||
|
const source = resolve(process.argv[2] || 'fluxer-src');
|
||||||
|
const names = readFileSync(resolve(patchDir, 'series'), 'utf8')
|
||||||
|
.replace(/^\uFEFF/, '').split(/\r?\n/).filter(Boolean);
|
||||||
|
if (!names.length) throw new Error('The patch series is empty');
|
||||||
|
for (const name of names) {
|
||||||
|
if (!/^[0-9]{4}-[a-z0-9-]+\.patch$/.test(name)) throw new Error(`Invalid patch name: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows checkout or an editor may supply CRLF despite .gitattributes.
|
||||||
|
// Normalize copies, retaining the original patch files and meaningful whitespace.
|
||||||
|
const temporaryDir = mkdtempSync(join(tmpdir(), 'gscord-patches-'));
|
||||||
|
try {
|
||||||
|
const patches = names.map((name) => {
|
||||||
|
const text = readFileSync(resolve(patchDir, name), 'utf8')
|
||||||
|
.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
|
||||||
|
const target = join(temporaryDir, name);
|
||||||
|
writeFileSync(target, text, 'utf8');
|
||||||
|
return target;
|
||||||
|
});
|
||||||
|
function apply(...options) {
|
||||||
|
const result = spawnSync('git', ['-C', source, 'apply', '--whitespace=error-all', ...options, ...patches], {
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
if (result.error) throw result.error;
|
||||||
|
if (result.status !== 0) throw new Error(`git apply ${options.join(' ')} failed (${result.status})`);
|
||||||
|
}
|
||||||
|
apply('--check');
|
||||||
|
if (!process.argv.includes('--check')) apply();
|
||||||
|
console.log(`Validated ${patches.length} GSCORD patches${process.argv.includes('--check') ? '' : ' and applied them'}.`);
|
||||||
|
} finally {
|
||||||
|
rmSync(temporaryDir, {recursive: true, force: true});
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {readFileSync} from 'node:fs';
|
||||||
|
import {stripTypeScriptTypes} from 'node:module';
|
||||||
|
import {resolve} from 'node:path';
|
||||||
|
import {pathToFileURL} from 'node:url';
|
||||||
|
import vm from 'node:vm';
|
||||||
|
import {test} from 'node:test';
|
||||||
|
|
||||||
|
const source = resolve(process.env.FLUXER_SOURCE || 'fluxer-src');
|
||||||
|
const desktop = resolve(source, 'fluxer_desktop');
|
||||||
|
const read = (file) => readFileSync(resolve(desktop, file), 'utf8');
|
||||||
|
const {parseGscordRelease} = await import(pathToFileURL(resolve(desktop, 'src/main/GscordRelease.ts')));
|
||||||
|
const compatibility = read('src/preload/GscordCompatibility.ts');
|
||||||
|
function evaluate(globals = {}) {
|
||||||
|
return vm.createContext({...globals});
|
||||||
|
}
|
||||||
|
function run(ts, context) {
|
||||||
|
vm.runInContext(stripTypeScriptTypes(ts).replace(/^export /gm, ''), context);
|
||||||
|
}
|
||||||
|
function rtcContext() {
|
||||||
|
class Peer {
|
||||||
|
constructor(config) { this.config = config; }
|
||||||
|
setConfiguration(config) { this.config = config; }
|
||||||
|
}
|
||||||
|
const context = evaluate({window: {RTCPeerConnection: Peer}, console: {info() {}}});
|
||||||
|
run(compatibility, context);
|
||||||
|
context.installForceAllIceTransportPolicy();
|
||||||
|
return {context, Peer, Wrapped: context.window.RTCPeerConnection};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('URLs, homepage and initial server use GSCORD', () => {
|
||||||
|
const constants = read('src/common/Constants.ts');
|
||||||
|
for (const name of ['STABLE_APP_URL', 'CANARY_APP_URL', 'STABLE_MIGRATED_APP_ORIGIN', 'CANARY_MIGRATED_APP_ORIGIN']) {
|
||||||
|
assert.ok(constants.includes(`export const ${name} = 'https://ds.gandc.ru';`));
|
||||||
|
}
|
||||||
|
assert.equal(JSON.parse(read('package.json')).homepage, 'https://ds.gandc.ru');
|
||||||
|
const discovery = read('src/main/DesktopRuntimeDiscovery.ts');
|
||||||
|
const start = discovery.indexOf('export function desktopRuntimeInitialInput');
|
||||||
|
const code = discovery.slice(start, discovery.indexOf('\nasync function fetchRuntimePlan', start));
|
||||||
|
let override = null;
|
||||||
|
const context = evaluate({BUILD_CHANNEL: 'stable', CHANNEL_APP_URLS: {stable: 'https://ds.gandc.ru'},
|
||||||
|
getLaunchInstanceEndpointOverride: () => override});
|
||||||
|
run(code, context);
|
||||||
|
assert.equal(context.desktopRuntimeInitialInput(), 'https://ds.gandc.ru');
|
||||||
|
override = 'https://another.example';
|
||||||
|
assert.equal(context.desktopRuntimeInitialInput(), override);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ICE constructor overrides relay without mutating caller configuration', () => {
|
||||||
|
const {Peer, Wrapped} = rtcContext();
|
||||||
|
const config = Object.freeze({iceTransportPolicy: 'relay', iceServers: [{urls: 'stun:example.test'}]});
|
||||||
|
const peer = new Wrapped(config);
|
||||||
|
assert.equal(peer.config.iceTransportPolicy, 'all');
|
||||||
|
assert.equal(config.iceTransportPolicy, 'relay');
|
||||||
|
assert.equal(peer.config.iceServers, config.iceServers);
|
||||||
|
assert.ok(peer instanceof Peer);
|
||||||
|
assert.ok(peer instanceof Wrapped);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ICE setConfiguration overrides relay and preserves native validation path', () => {
|
||||||
|
const {Wrapped} = rtcContext();
|
||||||
|
const peer = new Wrapped();
|
||||||
|
const config = Object.freeze({iceTransportPolicy: 'relay'});
|
||||||
|
peer.setConfiguration(config);
|
||||||
|
assert.equal(peer.config.iceTransportPolicy, 'all');
|
||||||
|
assert.equal(config.iceTransportPolicy, 'relay');
|
||||||
|
const other = {iceTransportPolicy: 'all'};
|
||||||
|
peer.setConfiguration(other);
|
||||||
|
assert.equal(peer.config, other);
|
||||||
|
peer.setConfiguration(undefined);
|
||||||
|
assert.equal(peer.config, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ICE proxy preserves subclass construction and default configuration', () => {
|
||||||
|
const {Wrapped} = rtcContext();
|
||||||
|
class Derived extends Wrapped {}
|
||||||
|
const peer = new Derived();
|
||||||
|
assert.ok(peer instanceof Derived);
|
||||||
|
assert.equal(peer.config, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ICE installer tolerates missing WebRTC', () => {
|
||||||
|
const context = evaluate({window: {}, console});
|
||||||
|
run(compatibility, context);
|
||||||
|
context.installForceAllIceTransportPolicy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('LiveKit workaround restores compression for RTC socket and suppresses it on close', () => {
|
||||||
|
class Compression {}
|
||||||
|
class Socket {
|
||||||
|
constructor(url) { this.url = url; }
|
||||||
|
addEventListener(name, callback) { this[name] = callback; }
|
||||||
|
}
|
||||||
|
const window = {WebSocket: Socket, CompressionStream: Compression, location: {href: 'https://ds.gandc.ru'}};
|
||||||
|
const context = evaluate({window, URL, console: {info() {}}});
|
||||||
|
run(compatibility, context);
|
||||||
|
context.installDisableLiveKitOfferWithJoin();
|
||||||
|
assert.equal(window.CompressionStream, undefined);
|
||||||
|
new window.WebSocket('wss://ds.gandc.ru/gateway');
|
||||||
|
assert.equal(window.CompressionStream, undefined);
|
||||||
|
const rtc = new window.WebSocket('wss://voice.example/rtc/v1?token=test');
|
||||||
|
assert.equal(window.CompressionStream, Compression);
|
||||||
|
rtc.close();
|
||||||
|
assert.equal(window.CompressionStream, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('preload installs both self-contained callbacks in main world', () => {
|
||||||
|
const preload = read('src/preload/index.ts');
|
||||||
|
for (const name of ['installForceAllIceTransportPolicy', 'installDisableLiveKitOfferWithJoin']) {
|
||||||
|
assert.ok(preload.includes(`contextBridge.executeInMainWorld({func: ${name}});`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('navigation allows exact SSO and callback origins but rejects lookalikes and reserved paths', () => {
|
||||||
|
const windowCode = read('src/main/Window.ts');
|
||||||
|
const start = windowCode.indexOf('function isAllowedSsoNavigation');
|
||||||
|
const end = windowCode.indexOf('function preventUntrustedChildNavigation', start);
|
||||||
|
const local = 'fluxer://app/';
|
||||||
|
const context = evaluate({URL, logger: {warn() {}},
|
||||||
|
isLocalAppURL: (url) => url.startsWith(local),
|
||||||
|
isLocalAppRendererDocumentURL: (url) => url === `${local}channels/@me`,
|
||||||
|
isTrustedOrigin: (url) => url === `${local}channels/@me`});
|
||||||
|
run(windowCode.slice(start, end), context);
|
||||||
|
for (const [url, blocked] of [
|
||||||
|
['https://sso.gandc.ru/login', false], ['https://ds.gandc.ru/callback', false],
|
||||||
|
[`${local}channels/@me`, false], [`${local}__reserved`, true],
|
||||||
|
['https://sso.gandc.ru.evil.test', true], ['http://sso.gandc.ru', true],
|
||||||
|
['https://sso.gandc.ru:8443', true], ['https://sso.gandc.ru@evil.test', true], ['invalid', true],
|
||||||
|
]) {
|
||||||
|
let prevented = false;
|
||||||
|
context.preventUntrustedNavigation({preventDefault() { prevented = true; }}, url, true);
|
||||||
|
assert.equal(prevented, blocked, url);
|
||||||
|
}
|
||||||
|
assert.equal((windowCode.match(/webContents.on\('will-redirect'/g) || []).length, 2);
|
||||||
|
const trusted = windowCode.slice(windowCode.indexOf('export function isTrustedOrigin'), windowCode.indexOf('function getSanitizedPath'));
|
||||||
|
assert.ok(!trusted.includes('isAllowedSsoNavigation'));
|
||||||
|
});
|
||||||
|
|
||||||
|
const asset = (name) => ({name, browser_download_url: `https://git.gandc.ru/assets/${name}`});
|
||||||
|
const release = {tag_name: 'v1.0.99', published_at: '2026-10-11T00:00:00Z', assets: [
|
||||||
|
asset('Fluxer-1.0.99-linux-x86_64.AppImage'), asset('Fluxer-1.0.99-linux-amd64.deb'),
|
||||||
|
asset('Fluxer-1.0.99-linux-arm64.AppImage'), asset('Fluxer-1.0.99-linux-arm64.deb'),
|
||||||
|
asset('Fluxer-1.0.99-portable-win-x64.exe'), asset('Fluxer-1.0.99-win-arm64.exe'),
|
||||||
|
asset('unrelated.zip'), null, {name: 'broken'},
|
||||||
|
]};
|
||||||
|
|
||||||
|
test('Gitea parser selects only the matching platform and architecture', () => {
|
||||||
|
for (const arch of ['x64', 'arm64']) {
|
||||||
|
const linux = parseGscordRelease(release, 'linux', arch);
|
||||||
|
assert.equal(linux.version, '1.0.99');
|
||||||
|
assert.equal(linux.pubDate, release.published_at);
|
||||||
|
assert.deepEqual(Object.keys(linux.files).sort(), ['appimage', 'deb']);
|
||||||
|
assert.ok(linux.files.appimage.url.includes(arch === 'x64' ? 'x86_64' : 'arm64'));
|
||||||
|
const win = parseGscordRelease(release, 'win32', arch);
|
||||||
|
assert.deepEqual(Object.keys(win.files), ['setup']);
|
||||||
|
assert.ok(win.files.setup.url.endsWith(`win-${arch}.exe`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Gitea parser accepts current portable filename and handles absent or malformed assets', () => {
|
||||||
|
const parsed = parseGscordRelease({tag_name: '1.0.100', assets: [asset('Fluxer-1.0.100-win-x64.exe')]}, 'win32', 'x64');
|
||||||
|
assert.ok(parsed.files.setup);
|
||||||
|
assert.deepEqual(parseGscordRelease({tag_name: 'v1.0.1'}, 'linux', 'x64').files, {});
|
||||||
|
for (const payload of [null, {}, {tag_name: ' '}, {tag_name: 'v'}]) assert.throws(() => parseGscordRelease(payload));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('manual updater offers actual Gitea assets rather than fabricated URLs', () => {
|
||||||
|
let code = read('src/main/UpdaterDownloads.ts').replace(/^import[\s\S]*?from '[^']+';\n/gm, '');
|
||||||
|
const context = evaluate({process: {platform: 'linux'}, DESKTOP_DOWNLOAD_ARCH: 'x64',
|
||||||
|
DESKTOP_ARTIFACT_PRODUCT_NAME: 'Fluxer', LINUX_MANUAL_ARCH_TOKENS: {deb: {x64: 'amd64'}},
|
||||||
|
LINUX_MANUAL_FORMAT_EXTENSIONS: {deb: '.deb'}, isLinuxManualDesktopFormat: () => true});
|
||||||
|
run(code, context);
|
||||||
|
const info = {version: '1.0.99', files: {deb: {url: 'https://git.gandc.ru/assets/real.deb', sha256: null}}};
|
||||||
|
const options = context.getManualDownloadOptions(info);
|
||||||
|
assert.equal(options.length, 1);
|
||||||
|
assert.equal(options[0].url, info.files.deb.url);
|
||||||
|
assert.equal(context.getManualDownloadUrl({version: '1.0.99', files: {}}), 'https://git.gandc.ru/gscord/gscord-desktop/releases');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GSCORD portable Windows and Linux including AppImage use manual updates', () => {
|
||||||
|
const code = read('src/main/ShellUpdateCapability.ts');
|
||||||
|
const start = code.indexOf('export function decideShellUpdatePlan');
|
||||||
|
const context = evaluate({ShellUpdateCapability: {MANUAL_DOWNLOAD: 'manual', MANAGED_PACKAGE: 'managed', SELF_UPDATE: 'self'}});
|
||||||
|
run(code.slice(start, code.indexOf('function resolveRunningAppImage', start)), context);
|
||||||
|
const runtime = {packaged: true, channel: 'stable', flatpak: false, portable: false, appImage: {path: 'test'}};
|
||||||
|
for (const platform of ['win32', 'linux']) assert.equal(context.decideShellUpdatePlan({...runtime, platform}).capability, 'manual');
|
||||||
|
assert.equal(context.decideShellUpdatePlan({...runtime, platform: 'linux', flatpak: true}).capability, 'managed');
|
||||||
|
assert.equal(context.decideShellUpdatePlan({...runtime, platform: 'linux', packaged: false}).reason, 'unpackaged');
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import {spawnSync} from 'node:child_process';
|
||||||
|
import {appendFileSync, existsSync, mkdirSync, readFileSync} from 'node:fs';
|
||||||
|
import {resolve} from 'node:path';
|
||||||
|
|
||||||
|
const source = resolve('fluxer-src');
|
||||||
|
function git(...args) {
|
||||||
|
const result = spawnSync('git', args, {encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit']});
|
||||||
|
if (result.error) throw result.error;
|
||||||
|
if (result.status !== 0) throw new Error(`git ${args[0]} failed (${result.status})`);
|
||||||
|
if (result.stdout) process.stdout.write(result.stdout);
|
||||||
|
return result.stdout.trim();
|
||||||
|
}
|
||||||
|
// checkout cleans the job workspace; never reuse a source tree from a previous run.
|
||||||
|
if (existsSync(source)) throw new Error(`Source directory already exists: ${source}`);
|
||||||
|
mkdirSync(source);
|
||||||
|
git('-C', source, 'init');
|
||||||
|
git('-C', source, 'config', 'core.autocrlf', 'false');
|
||||||
|
git('-C', source, 'remote', 'add', 'origin', 'https://github.com/fluxerapp/fluxer.git');
|
||||||
|
const ref = process.env.UPSTREAM_SHA || 'main';
|
||||||
|
if (ref !== 'main' && !/^[0-9a-f]{40}$/.test(ref)) throw new Error('UPSTREAM_SHA must be a full commit SHA');
|
||||||
|
git('-C', source, 'fetch', '--depth=1', 'origin', ref);
|
||||||
|
git('-C', source, 'checkout', '--detach', 'FETCH_HEAD');
|
||||||
|
const sha = git('-C', source, 'rev-parse', 'HEAD');
|
||||||
|
const packageManager = JSON.parse(readFileSync(resolve(source, 'package.json'), 'utf8')).packageManager;
|
||||||
|
console.log(`Fluxer revision: ${sha}; package manager: ${packageManager}`);
|
||||||
|
if (process.env.GITHUB_OUTPUT) appendFileSync(process.env.GITHUB_OUTPUT, `sha=${sha}\n`);
|
||||||
|
if (process.env.GITHUB_ENV) appendFileSync(process.env.GITHUB_ENV, `WORKDIR=${source}\n`);
|
||||||
Reference in new issue
Block a user