diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c9d4c616d..bded38a75 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -335,6 +335,108 @@ jobs: Copy-Item $installer.FullName $offlineName -Force gh release upload "${env:GITHUB_REF_NAME}" $offlineName --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 + # the oldest pullable Ubuntu container for each arch). + needs: build + strategy: + fail-fast: false + matrix: + include: + - platform: ubuntu-22.04 + target: x86_64-unknown-linux-musl + arch: x64 + rustflags: "-C target-feature=+crt-static" + - platform: ubuntu-22.04-arm + target: aarch64-unknown-linux-musl + arch: arm64 + # 64K max-page-size keeps the binary compatible with 4K/16K/64K + # page-size ARM64 kernels. + rustflags: "-C target-feature=+crt-static -C link-arg=-z -C link-arg=max-page-size=65536" + runs-on: ${{ matrix.platform }} + 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: Setup Rust + uses: dtolnay/rust-toolchain@1.97.1 + with: + targets: ${{ matrix.target }} + + - name: Install cargo-zigbuild + run: pip3 install cargo-zigbuild + + - name: Rust cache + uses: swatinem/rust-cache@v2 + with: + workspaces: "./ -> target" + shared-key: static-browser-${{ matrix.target }} + cache-on-failure: true + + - name: Build frontend + run: pnpm build + + - name: Build static web binary + env: + RUSTFLAGS: ${{ matrix.rustflags }} + run: | + cargo zigbuild --release -p dbx-web --target ${{ matrix.target }} \ + --no-default-features --features "duckdb-bundled,mq-admin" + + - name: Package static browser runtime + env: + DBX_STATIC_TARGET: ${{ matrix.target }} + run: | + chmod +x scripts/package-web-static.sh scripts/verify-web-static.sh + ./scripts/package-web-static.sh + + - name: Verify inside oldest available Ubuntu container + run: | + VERIFY_IMAGE="" + for image in ubuntu:14.04 ubuntu:16.04; do + if docker pull "$image" >/dev/null 2>&1; then + VERIFY_IMAGE="$image" + break + fi + done + if [ -z "$VERIFY_IMAGE" ]; then + echo "no legacy Ubuntu image available for $(uname -m)" >&2 + exit 1 + fi + echo "Verifying in $VERIFY_IMAGE" + docker run --rm -v "$PWD:/workspace" -w /workspace "$VERIFY_IMAGE" \ + bash scripts/verify-web-static.sh \ + "dist-web-static/dbx-linux-${{ matrix.arch }}-browser-static.tar.gz" + + - name: Upload release asset + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION="${GITHUB_REF_NAME#v}" + PACKAGE_VERSION="$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | select(.name == "dbx-web") | .version')" + if [ "$PACKAGE_VERSION" != "$VERSION" ]; then + echo "Release tag version $VERSION does not match dbx-web package version $PACKAGE_VERSION" >&2 + exit 1 + fi + SOURCE="dist-web-static/dbx-linux-${{ matrix.arch }}-browser-static.tar.gz" + ASSET="DBX_${VERSION}_${{ matrix.arch }}-browser-static.tar.gz" + CHECKSUM="${ASSET}.sha256" + cp "$SOURCE" "$ASSET" + sha256sum "$ASSET" | tee "$CHECKSUM" + gh release upload "${GITHUB_REF_NAME}" "$ASSET" "$CHECKSUM" \ + --repo "${GITHUB_REPOSITORY}" --clobber + cleanup-release-signatures: needs: build runs-on: ubuntu-latest @@ -424,7 +526,7 @@ jobs: publish: # Keep the release as a draft until every platform and the Nix package pass. - needs: [cleanup-release-signatures, docker-manifest, jdbc-plugin, nix-packaging] + needs: [cleanup-release-signatures, docker-manifest, jdbc-plugin, nix-packaging, static-browser] runs-on: ubuntu-latest steps: - name: Publish draft release diff --git a/crates/dbx-core/Cargo.toml b/crates/dbx-core/Cargo.toml index e4397c102..d05abf5b4 100644 --- a/crates/dbx-core/Cargo.toml +++ b/crates/dbx-core/Cargo.toml @@ -32,10 +32,11 @@ test = false bench = false [features] -default = ["duckdb-bundled", "mq-admin", "sqlite-sqlcipher"] +default = ["duckdb-bundled", "mq-admin", "sqlite-sqlcipher", "system-fonts"] duckdb-bundled = ["duckdb/bundled", "duckdb/parquet"] mq-admin = [] sqlite-sqlcipher = ["rusqlite/bundled-sqlcipher-vendored-openssl"] +system-fonts = ["font-kit"] [dependencies] serde = { version = "1.0", features = ["derive"] } @@ -88,7 +89,7 @@ argon2 = "0.5" sha2 = "0.10" async-trait = "0.1" bytes = "1" -font-kit = "0.14.3" +font-kit = { version = "0.14.3", optional = true } zip = { version = "4", default-features = false, features = ["deflate"] } sysinfo = { version = "0.32", features = ["system"] } tempfile = "3" diff --git a/crates/dbx-core/src/jdbc.rs b/crates/dbx-core/src/jdbc.rs index fee44fd86..98a477bde 100644 --- a/crates/dbx-core/src/jdbc.rs +++ b/crates/dbx-core/src/jdbc.rs @@ -318,6 +318,7 @@ pub fn uninstall_jdbc_plugin(plugins_root: &Path) -> Result Vec { let source = font_kit::source::SystemSource::new(); match source.all_families() { @@ -335,6 +336,11 @@ pub fn list_system_fonts() -> Vec { } } +#[cfg(not(feature = "system-fonts"))] +pub fn list_system_fonts() -> Vec { + vec![] +} + // ---- Internal helpers ---- fn jdbc_drivers_dir(plugins_root: &Path) -> PathBuf { diff --git a/crates/dbx-web/Cargo.toml b/crates/dbx-web/Cargo.toml index f0edb2ca5..fa4b69fd5 100644 --- a/crates/dbx-web/Cargo.toml +++ b/crates/dbx-web/Cargo.toml @@ -9,10 +9,11 @@ name = "dbx-web" path = "src/main.rs" [features] -default = ["duckdb-bundled", "mq-admin", "sqlite-sqlcipher"] +default = ["duckdb-bundled", "mq-admin", "sqlite-sqlcipher", "system-fonts"] duckdb-bundled = ["dbx-core/duckdb-bundled"] mq-admin = ["dbx-core/mq-admin"] sqlite-sqlcipher = ["dbx-core/sqlite-sqlcipher"] +system-fonts = ["dbx-core/system-fonts"] [dependencies] dbx-core = { path = "../dbx-core", default-features = false } diff --git a/scripts/package-web-static.sh b/scripts/package-web-static.sh new file mode 100644 index 000000000..b8509cccd --- /dev/null +++ b/scripts/package-web-static.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Package a fully static musl-linked dbx-web binary together with the built +# frontend into a portable tarball that runs on any Linux (no glibc needed). + +target="${DBX_STATIC_TARGET:-aarch64-unknown-linux-musl}" +dist_dir="${DBX_FRONTEND_DIST:-dist}" +output_dir="${DBX_STATIC_OUTPUT_DIR:-dist-web-static}" + +case "$target" in + x86_64-unknown-linux-musl*) + arch_label="x64" + ;; + aarch64-unknown-linux-musl*) + arch_label="arm64" + ;; + *) + echo "unsupported static web target: $target" >&2 + exit 2 + ;; +esac + +binary="${DBX_WEB_BINARY:-target/${target}/release/dbx-web}" +package_name="${DBX_STATIC_PACKAGE_NAME:-dbx-linux-${arch_label}-browser-static}" +package_dir="${output_dir}/${package_name}" +tarball="${output_dir}/${package_name}.tar.gz" + +if [ ! -x "$binary" ]; then + echo "missing static dbx-web binary: $binary" >&2 + 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 + echo "dbx-web is not fully static: ELF has a program interpreter" >&2 + exit 1 +fi + +if readelf -d "$binary" 2>/dev/null | grep -q 'Shared library:'; then + readelf -d "$binary" | grep 'Shared library:' >&2 || true + echo "dbx-web is not fully static: ELF has dynamic shared library dependencies" >&2 + exit 1 +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/" + +cat > "$package_dir/dbx" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do + DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)" + SOURCE="$(readlink "$SOURCE")" + [[ "$SOURCE" != /* ]] && SOURCE="$DIR/$SOURCE" +done +ROOT="$(cd -P "$(dirname "$SOURCE")" && pwd)" +export DBX_PACKAGE_ROOT="$ROOT" +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:-/}" +case "$base_path" in + "") base_path="/" ;; + /*) ;; + *) base_path="/$base_path" ;; +esac +printf 'DBX browser UI: http://127.0.0.1:%s%s\n' "$port" "$base_path" +cd "$ROOT" +exec "$ROOT/bin/dbx-web-bin" "$@" +EOF +chmod +x "$package_dir/dbx" +ln -sfn dbx "$package_dir/dbx-web" + +cat > "$package_dir/README.txt" <" >&2 + exit 2 +fi + +verify_seconds="${DBX_STATIC_VERIFY_SECONDS:-30}" +runtime_dir="${DBX_STATIC_VERIFY_DIR:-/tmp/dbx-web-static-verify}" + +if [ ! -f "$package_tarball" ]; then + echo "missing package tarball: $package_tarball" >&2 + exit 1 +fi + +if ! command -v curl >/dev/null 2>&1 || ! command -v readelf >/dev/null 2>&1; then + export DEBIAN_FRONTEND=noninteractive + # Archived releases (14.04, etc.) have moved to old-releases.ubuntu.com + if [ -f /etc/apt/sources.list ] && ! apt-get update 2>/dev/null; then + sed -i 's|archive\.ubuntu\.com|old-releases.ubuntu.com|g' /etc/apt/sources.list + sed -i 's|security\.ubuntu\.com|old-releases.ubuntu.com|g' /etc/apt/sources.list + apt-get -o Acquire::Check-Valid-Until=false update + fi + apt-get install -y --no-install-recommends ca-certificates bash binutils curl procps + rm -rf /var/lib/apt/lists/* +fi + +rm -rf "$runtime_dir" +mkdir -p "$runtime_dir" +tar -xzf "$package_tarball" -C "$runtime_dir" --strip-components=1 + +for required_runtime_path in \ + dbx \ + bin/dbx-web-bin \ + dist/index.html; do + if [ ! -e "$runtime_dir/$required_runtime_path" ]; then + echo "missing packaged static web runtime path: $required_runtime_path" >&2 + exit 1 + fi +done + +if readelf -l "$runtime_dir/bin/dbx-web-bin" | grep -q 'Requesting program interpreter'; then + readelf -l "$runtime_dir/bin/dbx-web-bin" | grep 'Requesting program interpreter' >&2 || true + echo "packaged dbx-web is not static: ELF has a program interpreter" >&2 + exit 1 +fi + +if readelf -d "$runtime_dir/bin/dbx-web-bin" 2>/dev/null | grep -q 'Shared library:'; then + readelf -d "$runtime_dir/bin/dbx-web-bin" | grep 'Shared library:' >&2 || true + echo "packaged dbx-web is not static: ELF has dynamic shared library dependencies" >&2 + exit 1 +fi + +cd "$runtime_dir" +export RUST_BACKTRACE="${RUST_BACKTRACE:-full}" +export DBX_DISABLE_PASSWORD="${DBX_DISABLE_PASSWORD:-1}" +export DBX_PORT="${DBX_PORT:-4224}" + +echo "static web verification page size: $(getconf PAGE_SIZE 2>/dev/null || echo unknown)" +./dbx >/tmp/dbx-web-static.log 2>&1 & +pid=$! +cleanup() { + kill "$pid" >/dev/null 2>&1 || true + wait "$pid" >/dev/null 2>&1 || true +} +trap cleanup EXIT + +for _ in $(seq 1 "$verify_seconds"); do + if ! kill -0 "$pid" 2>/dev/null; then + cat /tmp/dbx-web-static.log || true + wait "$pid" + exit 1 + fi + + if curl -fsS "http://127.0.0.1:${DBX_PORT}/" >/tmp/dbx-web-static-index.html 2>/dev/null; then + curl -fsS "http://127.0.0.1:${DBX_PORT}/api/auth/check" >/tmp/dbx-web-static-auth.json + grep -q '"authenticated":true' /tmp/dbx-web-static-auth.json + echo "static web verification HTTP check passed" + tail -40 /tmp/dbx-web-static.log || true + exit 0 + fi + + sleep 1 +done + +cat /tmp/dbx-web-static.log || true +echo "static dbx-web did not become ready within ${verify_seconds}s" >&2 +exit 1 diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index b7468dfa6..34f3ab201 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -13,10 +13,11 @@ name = "dbx_lib" crate-type = ["staticlib", "cdylib", "rlib"] [features] -default = ["duckdb-bundled", "mq-admin", "sqlite-sqlcipher"] +default = ["duckdb-bundled", "mq-admin", "sqlite-sqlcipher", "system-fonts"] duckdb-bundled = ["duckdb", "duckdb/parquet", "dbx-core/duckdb-bundled"] mq-admin = ["dbx-core/mq-admin"] sqlite-sqlcipher = ["dbx-core/sqlite-sqlcipher"] +system-fonts = ["dbx-core/system-fonts"] [build-dependencies] tauri-build = { version = "2.5.6", features = [] }