feat(web): embed static dist and add win7 static browser release

This commit is contained in:
zhouwenqing
2026-09-24 23:22:40 +08:00
committed by GitHub
parent 37fc2f8a9b
commit 3640f6827e
7 changed files with 300 additions and 27 deletions
+41
View File
@@ -0,0 +1,41 @@
@echo off
setlocal EnableDelayedExpansion
:: 脚本所在目录(等价于 ROOT)
set ROOT=%~dp0
:: 去掉末尾反斜杠
set ROOT=%ROOT:~0,-1%
set DBX_PACKAGE_ROOT=%ROOT%
:: static dir
if "%DBX_STATIC_DIR%"=="" (
:: dist静态资源文件已直接嵌入到二进制文件中,环境变量中没有设置的话,直接用二进制中的静态文件
rem set DBX_STATIC_DIR=%ROOT%\dist
)
:: data dir
if "%DBX_DATA_DIR%"=="" (
set DBX_DATA_DIR=%ROOT%\data
)
:: port
if "%DBX_PORT%"=="" (
set DBX_PORT=4224
)
:: base path
set BASE_PATH=%DBX_PUBLIC_BASE_PATH%
if "%BASE_PATH%"=="" set BASE_PATH=/
:: 如果不是以 / 开头,补上 /
echo %BASE_PATH% | findstr /r "^/" >nul || (
set BASE_PATH=/!BASE_PATH!
)
echo DBX_DATA_DIR=%DBX_DATA_DIR%
echo DBX browser UI: http://127.0.0.1:%DBX_PORT%%BASE_PATH%
cd /d "%ROOT%"
:: 执行程序
dbx-web.exe %*
+86 -2
View File
@@ -703,6 +703,89 @@ jobs:
Remove-Item -LiteralPath $workDir -Recurse -Force
}
build-windows-7-static-browser:
needs: release-ready
runs-on: windows-2022
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-win7-windows-msvc
arch: x86_64
env:
CARGO_INCREMENTAL: "0"
RUSTFLAGS: -C debuginfo=line-tables-only -C target-feature=+crt-static
steps:
- uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
- name: Setup pnpm
uses: pnpm/action-setup@v6
- name: Install frontend dependencies
run: pnpm install
- name: Build frontend
run: pnpm build
- name: Setup Rust for Windows 7
uses: dtolnay/rust-toolchain@nightly
with:
toolchain: nightly-2026-07-22
components: rust-src
- name: Install NASM
run: choco install nasm -y --no-progress
- name: Add NASM to PATH
shell: pwsh
run: Add-Content $env:GITHUB_PATH "C:\Program Files\NASM"
- name: Build DBX for Windows 7 ${{ matrix.arch }}
shell: pwsh
run: |
cargo build --locked --package dbx-web --release --no-default-features --features embed-static,duckdb-sidecar,dynamodb,mq-admin,dbx-core/sqlite-bundled --target ${{ matrix.target }} -Z build-std=std,panic_abort
- name: Audit Windows 7 PE imports
shell: pwsh
run: ./.github/scripts/assert-win7-pe-compat.ps1 -BinaryPath target/${{ matrix.target }}/release/dbx-web.exe
- name: Upload Windows portable ${{ matrix.arch }} ZIP
timeout-minutes: 20
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$version = "${env:GITHUB_REF_NAME}".TrimStart("v")
$arch = "${{ matrix.arch }}"
$portableRoot = "portable"
$portableDir = Join-Path $portableRoot "DBX_${version}_${arch}"
$zipName = "dbx-web_${version}_${arch}-browser-static.zip"
$zipSha256 = "dbx-web_${version}_${arch}-browser-static.zip.sha256"
$exePath = "target/${{ matrix.target }}/release/dbx-web.exe"
if (!(Test-Path $exePath)) {
Write-Error "Missing Windows executable: $exePath"
exit 1
}
New-Item -ItemType Directory -Force -Path $portableDir | Out-Null
Copy-Item $exePath (Join-Path $portableDir "dbx-web.exe") -Force
Copy-Item "LICENSE" (Join-Path $portableDir "LICENSE") -Force
Copy-Item "README.md" (Join-Path $portableDir "README.md") -Force
Copy-Item ".github\scripts\start-dbx-web.bat" (Join-Path $portableDir "start.bat") -Force
Compress-Archive -Path (Join-Path $portableDir "*") -DestinationPath $zipName -Force
$zip_sha256 = (Get-FileHash -LiteralPath $zipName -Algorithm SHA256).Hash.ToLowerInvariant()
Set-Content -Path $zipSha256 -Value $zip_sha256 -Encoding utf8NoBOM
gh release upload "${env:GITHUB_REF_NAME}" $zipName $zipSha256 --repo "${env:GITHUB_REPOSITORY}" --clobber
static-browser:
# Fully static musl builds of the browser (dbx-web) variant. No glibc
# dependency, so the tarball runs on any Linux distribution (verified in
@@ -799,7 +882,7 @@ jobs:
RUSTFLAGS: ${{ matrix.rustflags }}
run: |
cargo zigbuild --release -p dbx-web --target ${{ matrix.target }} \
--no-default-features --features "duckdb-sidecar,dynamodb,mq-admin,dbx-core/sqlite-bundled"
--no-default-features --features "embed-static,duckdb-sidecar,dynamodb,mq-admin,dbx-core/sqlite-bundled"
- name: Package static browser runtime
env:
@@ -938,7 +1021,8 @@ jobs:
publish:
# Keep the release as a draft until every release-blocking platform job passes.
# Nix packaging is advisory and runs independently from publication.
# Nix packaging and the Windows 7 static-browser build are advisory and
# upload their release assets independently of publication.
needs: [cleanup-release-signatures, docker-manifest, jdbc-plugin, static-browser]
runs-on: ubuntu-latest
steps:
Generated
+37
View File
@@ -2408,9 +2408,11 @@ dependencies = [
"dbx-mcp",
"futures",
"log",
"mime_guess",
"pbkdf2 0.12.2",
"redis",
"reqwest 0.12.28",
"rust-embed",
"rustls 0.23.43",
"serde",
"serde_json",
@@ -7654,6 +7656,41 @@ dependencies = [
"wasm-bindgen-futures",
]
[[package]]
name = "rust-embed"
version = "8.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9e7760e252aaba7b09f4be00e36476cf585bdb68a53552ac954cdf504ab4bc9"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
"walkdir",
]
[[package]]
name = "rust-embed-impl"
version = "8.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bcfc4d6f53af43755f7a723e4b6b8794fcce052a178dd8c6c1dadc5f5343097"
dependencies = [
"mime_guess",
"proc-macro2",
"quote",
"rust-embed-utils",
"syn 2.0.119",
"walkdir",
]
[[package]]
name = "rust-embed-utils"
version = "8.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ffa149f6aa81b58a5b3011d01a857c4ed12c7a732d2c51947a4c7c692185f0"
dependencies = [
"sha2 0.11.0",
"walkdir",
]
[[package]]
name = "rust-ini"
version = "0.21.3"
+7
View File
@@ -15,6 +15,10 @@ dynamodb = ["dbx-core/dynamodb"]
mq-admin = ["dbx-core/mq-admin", "dbx-mcp/mq-admin"]
sqlite-sqlcipher = ["dbx-core/sqlite-sqlcipher"]
system-fonts = ["dbx-core/system-fonts"]
# Embed the frontend build output (../../dist) into the binary. When enabled
# and DBX_STATIC_DIR is not set, the embedded files are served by default.
# Requires the frontend to have been built before invoking cargo.
embed-static = ["dep:rust-embed", "dep:mime_guess"]
[dependencies]
dbx-core = { path = "../dbx-core", default-features = false, features = ["openapi"] }
@@ -42,6 +46,9 @@ base64 = "0.22"
pbkdf2 = "0.12"
sha2 = "0.10"
tempfile = "3"
rust-embed = { version = "8", features = ["debug-embed"], optional = true }
mime_guess = { version = "2", optional = true }
[dev-dependencies]
reqwest = { version = "0.12", default-features = false, features = ["multipart"] }
+126 -16
View File
@@ -159,12 +159,96 @@ where
)
}
fn mount_public_base_path(mut app: Router, public_base_path: &str, static_dir: Option<&std::path::Path>) -> Router {
if let Some(static_dir) = static_dir {
use tower_http::services::{ServeDir, ServeFile};
let index_path = static_dir.join("index.html");
let serve_dir = ServeDir::new(static_dir).not_found_service(ServeFile::new(index_path));
app = app.fallback_service(serve_dir);
/// Frontend build output compiled into the binary by the `embed-static` feature.
/// The folder is relative to this crate's manifest (crates/dbx-web) and points
/// at the workspace root `dist` directory produced by the frontend build.
#[cfg(feature = "embed-static")]
#[derive(rust_embed::RustEmbed)]
#[folder = "../../dist"]
struct EmbeddedStaticAssets;
/// Serves an embedded static asset, falling back to `index.html` so that
/// client-side (SPA) routes resolve like they do with the disk-based ServeDir.
#[cfg(feature = "embed-static")]
async fn serve_embedded_asset(uri: Uri) -> axum::response::Response {
use axum::http::header;
let path = uri.path().trim_start_matches('/');
if let Some(asset) = EmbeddedStaticAssets::get(path) {
let content_type = mime_guess::from_path(path).first_or_octet_stream();
let mut response = axum::response::Response::builder()
.header(header::CONTENT_TYPE, content_type.as_ref())
.body(axum::body::Body::from(asset.data.into_owned()))
.expect("valid embedded asset response");
// Vite emits content-hashed, immutable files under assets/. The HTML
// entry point must always revalidate so binary upgrades take effect;
// other files (favicon, fonts) keep browser heuristic caching, which
// matches tower-http's ServeDir behaviour.
let cache_control = if path.starts_with("assets/") {
Some("public, max-age=31536000, immutable")
} else if path == "index.html" {
Some("no-cache")
} else {
None
};
if let Some(cache_control) = cache_control {
response.headers_mut().insert(
header::CACHE_CONTROL,
header::HeaderValue::from_str(cache_control).expect("valid cache-control"),
);
}
response
} else {
serve_embedded_index().await
}
}
/// Serves the embedded SPA entry point. It must never be cached aggressively so
/// that upgraded binaries are not shadowed by an old index referencing removed
/// hashed assets.
#[cfg(feature = "embed-static")]
async fn serve_embedded_index() -> axum::response::Response {
use axum::http::header;
let Some(index) = EmbeddedStaticAssets::get("index.html") else {
// Mirrors the disk-based variant, where a missing index.html surfaces
// as a 404 from the not-found service instead of crashing the handler.
return axum::response::Response::builder()
.status(axum::http::StatusCode::NOT_FOUND)
.header(header::CONTENT_TYPE, "text/plain; charset=utf-8")
.body(axum::body::Body::from("embedded dist does not contain index.html"))
.expect("valid missing index response");
};
axum::response::Response::builder()
.header(header::CONTENT_TYPE, "text/html; charset=utf-8")
.header(header::CACHE_CONTROL, "no-cache")
.body(axum::body::Body::from(index.data.into_owned()))
.expect("valid embedded index response")
}
/// Selects where the static frontend files are served from.
#[derive(Clone, Copy)]
enum StaticSource<'a> {
/// Serve files from a directory on disk (DBX_STATIC_DIR).
Dir(&'a std::path::Path),
/// Serve the dist files compiled into the binary (`embed-static` feature).
#[cfg(feature = "embed-static")]
Embedded,
}
fn mount_static_assets(mut app: Router, public_base_path: &str, source: Option<StaticSource<'_>>) -> Router {
match source {
Some(StaticSource::Dir(static_dir)) => {
use tower_http::services::{ServeDir, ServeFile};
let index_path = static_dir.join("index.html");
let serve_dir = ServeDir::new(static_dir).not_found_service(ServeFile::new(index_path));
app = app.fallback_service(serve_dir);
}
#[cfg(feature = "embed-static")]
Some(StaticSource::Embedded) => {
app = app.fallback(serve_embedded_asset);
}
None => {}
}
if public_base_path == "/" {
@@ -173,9 +257,16 @@ fn mount_public_base_path(mut app: Router, public_base_path: &str, static_dir: O
app = Router::new().nest(public_base_path, app);
app = add_public_base_path_redirect(app, public_base_path);
if let Some(static_dir) = static_dir {
use tower_http::services::ServeFile;
app = app.route_service(&format!("{public_base_path}/"), ServeFile::new(static_dir.join("index.html")));
match source {
Some(StaticSource::Dir(static_dir)) => {
use tower_http::services::ServeFile;
app = app.route_service(&format!("{public_base_path}/"), ServeFile::new(static_dir.join("index.html")));
}
#[cfg(feature = "embed-static")]
Some(StaticSource::Embedded) => {
app = app.route(&format!("{public_base_path}/"), get(serve_embedded_index));
}
None => {}
}
app
}
@@ -1278,7 +1369,23 @@ async fn serve() {
}
let static_dir = std::env::var_os("DBX_STATIC_DIR").map(std::path::PathBuf::from);
app = mount_public_base_path(app, &public_base_path, static_dir.as_deref());
// DBX_STATIC_DIR always wins (frontend development); otherwise serve the
// assets embedded into the binary when the embed-static feature is enabled.
let static_source = {
let dir_source = static_dir.as_deref().map(StaticSource::Dir);
#[cfg(feature = "embed-static")]
{
dir_source.or_else(|| {
tracing::info!("Serving embedded frontend assets (DBX_STATIC_DIR not set)");
Some(StaticSource::Embedded)
})
}
#[cfg(not(feature = "embed-static"))]
{
dir_source
}
};
app = mount_static_assets(app, &public_base_path, static_source);
// Bind address
let port: u16 = std::env::var("DBX_PORT").ok().and_then(|p| p.parse().ok()).unwrap_or(4224);
@@ -1320,8 +1427,8 @@ async fn serve() {
#[cfg(test)]
mod tests {
use super::{
mount_public_base_path, normalize_public_base_path, web_agent_dir_from_env, web_body_limit_bytes_from_value,
web_compression_predicate, XLSX_CONTENT_TYPE,
mount_static_assets, normalize_public_base_path, web_agent_dir_from_env, web_body_limit_bytes_from_value,
web_compression_predicate, StaticSource, XLSX_CONTENT_TYPE,
};
use crate::routes::table_import;
use axum::body::Body;
@@ -1493,10 +1600,10 @@ mod tests {
std::fs::write(static_dir.join("app.js"), "subpath asset").expect("write asset");
for public_base_path in ["/dbx", "/xxxx/rsu"] {
let router = mount_public_base_path(
let router = mount_static_assets(
Router::new().route("/api/ping", get(|| async { "pong" })),
public_base_path,
Some(&static_dir),
Some(StaticSource::Dir(&static_dir)),
);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("bind test listener");
let address = listener.local_addr().expect("test listener address");
@@ -1559,8 +1666,11 @@ mod tests {
std::fs::create_dir_all(&static_dir).expect("create static directory");
std::fs::write(static_dir.join("index.html"), "root index").expect("write index");
std::fs::write(static_dir.join("app.js"), "root asset").expect("write asset");
let router =
mount_public_base_path(Router::new().route("/api/ping", get(|| async { "pong" })), "/", Some(&static_dir));
let router = mount_static_assets(
Router::new().route("/api/ping", get(|| async { "pong" })),
"/",
Some(StaticSource::Dir(&static_dir)),
);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("bind test listener");
let address = listener.local_addr().expect("test listener address");
let server = tokio::spawn(async move {
+2 -7
View File
@@ -31,10 +31,6 @@ if [ ! -x "$binary" ]; then
exit 1
fi
if [ ! -f "${dist_dir}/index.html" ]; then
echo "missing frontend build output: ${dist_dir}/index.html" >&2
exit 1
fi
if readelf -l "$binary" | grep -q 'Requesting program interpreter'; then
readelf -l "$binary" | grep 'Requesting program interpreter' >&2 || true
@@ -51,12 +47,11 @@ fi
rm -rf "$output_dir"
mkdir -p \
"$package_dir/bin" \
"$package_dir/dist" \
"$package_dir/data"
cp "$binary" "$package_dir/bin/dbx-web-bin"
chmod +x "$package_dir/bin/dbx-web-bin"
cp -a "${dist_dir}/." "$package_dir/dist/"
#cp -a "${dist_dir}/." "$package_dir/dist/"
cat > "$package_dir/dbx" <<'EOF'
#!/usr/bin/env bash
@@ -69,7 +64,7 @@ while [ -h "$SOURCE" ]; do
done
ROOT="$(cd -P "$(dirname "$SOURCE")" && pwd)"
export DBX_PACKAGE_ROOT="$ROOT"
export DBX_STATIC_DIR="${DBX_STATIC_DIR:-$ROOT/dist}"
#export DBX_STATIC_DIR="${DBX_STATIC_DIR:-$ROOT/dist}"
export DBX_DATA_DIR="${DBX_DATA_DIR:-$ROOT/data}"
port="${DBX_PORT:-4224}"
base_path="${DBX_PUBLIC_BASE_PATH:-/}"
+1 -2
View File
@@ -38,8 +38,7 @@ tar -xzf "$package_tarball" -C "$runtime_dir" --strip-components=1
for required_runtime_path in \
dbx \
bin/dbx-web-bin \
dist/index.html; do
bin/dbx-web-bin ; do
if [ ! -e "$runtime_dir/$required_runtime_path" ]; then
echo "missing packaged static web runtime path: $required_runtime_path" >&2
exit 1