Disable V8 runtime by default (custom builds can still enable it) and add a truly static binary release for Linux using MUSL.

Building with MUSL required vendoring sqlite-vec and OpenSSL.

Also clean up no-longer-supported TypeScript guest scripts.
This commit is contained in:
Sebastian Jeltsch
2025-10-06 17:12:28 +02:00
parent 06ed63c896
commit 08bcf98e67
14 changed files with 40 additions and 297 deletions

View File

@@ -24,7 +24,7 @@ jobs:
generate_release_notes: true
body_path: ./CHANGELOG-release.md
release-linux:
release-linux-glibc:
needs: changelog
runs-on: ubuntu-latest
steps:
@@ -53,13 +53,13 @@ jobs:
CARGO_PROFILE_RELEASE_LTO=fat CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1 PNPM_OFFLINE=TRUE \
cargo build --target x86_64-unknown-linux-gnu --release --bin trail && \
cd ../.. && \
zip -r -j trailbase_${{ github.ref_name }}_x86_64_linux.zip target/x86_64-unknown-linux-gnu/release/trail CHANGELOG.md LICENSE
zip -r -j trailbase_${{ github.ref_name }}_x86_64_linux_glibc.zip target/x86_64-unknown-linux-gnu/release/trail CHANGELOG.md LICENSE
- name: Release binaries
uses: softprops/action-gh-release@v2
with:
fail_on_unmatched_files: true
files: trailbase_${{ github.ref_name }}_x86_64_linux.zip
files: trailbase_${{ github.ref_name }}_x86_64_linux_glibc.zip
release-linux-static:
needs: changelog
@@ -71,7 +71,7 @@ jobs:
- name: Install Dependencies
run: |
sudo apt-get update && \
sudo apt-get install -y --no-install-recommends curl libssl-dev pkg-config libclang-dev protobuf-compiler libprotobuf-dev zip
sudo apt-get install -y --no-install-recommends curl libssl-dev pkg-config libclang-dev protobuf-compiler libprotobuf-dev zip musl-tools
- uses: pnpm/action-setup@v4
with:
version: 9
@@ -81,22 +81,21 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
toolchain: 1.90.0
target: x86_64-unknown-linux-gnu
target: x86_64-unknown-linux-musl
default: true
- name: Rust Build
# NOTE: static linux builds fail with toolchains >1.86 if run from workspace root?!
run: |
cd crates/cli && \
RUSTFLAGS="-C target-feature=+crt-static" CARGO_PROFILE_RELEASE_LTO=fat CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1 PNPM_OFFLINE=TRUE \
cargo build --target x86_64-unknown-linux-gnu --release --bin trail && \
CARGO_PROFILE_RELEASE_LTO=fat CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1 PNPM_OFFLINE=TRUE \
cargo build --target x86_64-unknown-linux-musl --features=vendor-ssl --release --bin trail && \
cd ../.. && \
zip -r -j trailbase_${{ github.ref_name }}_x86_64_linux_static_glibc.zip target/x86_64-unknown-linux-gnu/release/trail CHANGELOG.md LICENSE
zip -r -j trailbase_${{ github.ref_name }}_x86_64_linux.zip target/x86_64-unknown-linux-musl/release/trail CHANGELOG.md LICENSE
- name: Release binaries
uses: softprops/action-gh-release@v2
with:
fail_on_unmatched_files: true
files: trailbase_${{ github.ref_name }}_x86_64_linux_static_glibc.zip
files: trailbase_${{ github.ref_name }}_x86_64_linux.zip
release-linux-auth-ui:
needs: changelog

3
.gitmodules vendored
View File

@@ -1,3 +1,6 @@
[submodule "vendor/sqlean/bundled/sqlean"]
path = crates/sqlean/bundled/sqlean
url = https://github.com/trailbaseio/sqlean
[submodule "vendor/sqlite-vec"]
path = vendor/sqlite-vec
url = https://github.com/ignatz/sqlite-vec.git

16
Cargo.lock generated
View File

@@ -4699,6 +4699,15 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-src"
version = "300.5.2+3.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d270b79e2926f5150189d475bc7e9d2c69f9c4697b185fa917d5a32b792d21b4"
dependencies = [
"cc",
]
[[package]]
name = "openssl-sys"
version = "0.9.109"
@@ -4707,6 +4716,7 @@ checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
dependencies = [
"cc",
"libc",
"openssl-src",
"pkg-config",
"vcpkg",
]
@@ -6715,11 +6725,10 @@ dependencies = [
[[package]]
name = "sqlite-vec"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ec77b84fb8dd5f0f8def127226db83b5d1152c5bf367f09af03998b76ba554a"
version = "0.1.7-alpha.2"
dependencies = [
"cc",
"rusqlite",
]
[[package]]
@@ -7936,6 +7945,7 @@ dependencies = [
"log",
"mimalloc",
"minijinja",
"openssl",
"reqwest",
"serde",
"serde_json",

View File

@@ -24,6 +24,7 @@ members = [
"examples/wasm-guest-rust",
"examples/coffee-vector-search/guests/rust",
"examples/collab-clicker-ssr/guests/rust",
"vendor/sqlite-vec/bindings/rust",
]
default-members = [
"crates/assets",
@@ -81,6 +82,7 @@ rand = { version = "^0.9.0" }
reqwest = { version = "0.12.8", default-features = false, features = ["rustls-tls", "json"] }
rusqlite = { version = "0.37.0", default-features = false, features = ["bundled", "column_decltype", "functions", "backup", "preupdate_hook"] }
rust-embed = { version = "8.4.0", default-features = false, features = ["mime-guess"] }
sqlite-vec = { path = "vendor/sqlite-vec/bindings/rust", default-features = false }
tokio = { version = "^1.38.0", default-features = false, features = ["macros", "net", "rt-multi-thread", "fs", "signal", "time", "sync"] }
tracing = { version = "0.1.40", default-features = false }
tracing-subscriber = { version = "0.3.18", default-features = false, features = ["smallvec", "std", "fmt", "json"] }

View File

@@ -1,198 +0,0 @@
import {
addCronCallback,
addPeriodicCallback,
addRoute,
execute,
htmlHandler,
jsonHandler,
parsePath,
query,
stringHandler,
transaction,
HttpError,
StatusCodes,
Transaction,
} from "../trailbase.js";
import type {
Blob,
JsonRequestType,
ParsedPath,
StringRequestType,
} from "../trailbase.d.ts";
addRoute(
"GET",
"/test",
stringHandler(async (req: StringRequestType) => {
const uri: ParsedPath = parsePath(req.uri);
const table = uri.query.get("table");
if (table) {
const rows = await query(`SELECT COUNT(*) FROM "${table}"`, []);
return `entries: ${rows[0][0]}`;
}
return `test: ${req.uri}`;
}),
);
addRoute(
"GET",
"/test/{table}",
stringHandler(async (req: StringRequestType) => {
const table = req.params["table"];
if (table) {
const rows = await query(`SELECT COUNT(*) FROM "${table}"`, []);
return `entries: ${rows[0][0]}`;
}
return `test: ${req.uri}`;
}),
);
addRoute(
"GET",
"/tx/{table}",
stringHandler(async (req: StringRequestType) => {
const table = req.params["table"];
if (table) {
const count = await transaction((tx: Transaction) => {
const rows = tx.query(`SELECT COUNT(*) FROM "${table}"`, []);
return rows[0][0] as number;
});
return `entries: ${count}`;
}
return `test: ${req.uri}`;
}),
);
addRoute(
"GET",
"/html",
htmlHandler((_req: StringRequestType) => {
return `
<html>
<body>
<h1>Html Handler</h1>
</body>
</html>
`;
}),
);
addRoute(
"GET",
"/json",
jsonHandler((_req: JsonRequestType) => {
return {
int: 5,
real: 4.2,
msg: "foo",
obj: {
nested: true,
},
};
}),
);
addRoute(
"GET",
"/error",
jsonHandler((_req: JsonRequestType) => {
throw new HttpError(StatusCodes.IM_A_TEAPOT, "I'm a teapot");
}),
);
addRoute(
"GET",
"/fetch",
stringHandler(async (req: StringRequestType) => {
const query = parsePath(req.uri).query;
const url = query.get("url");
if (url) {
const response = await fetch(url);
return await response.text();
}
throw new HttpError(StatusCodes.BAD_REQUEST, `Missing ?url param: ${req.params}`);
}),
);
addRoute(
"GET",
"/fibonacci",
stringHandler((req: StringRequestType) => {
const uri: ParsedPath = parsePath(req.uri);
const n = uri.query.get("n");
return fibonacci(n ? parseInt(n) : 40).toString();
}),
);
addRoute(
"GET",
"/addDeletePost",
stringHandler(async (_req: StringRequestType) => {
const userId: Blob = (
await query("SELECT id FROM _user WHERE email = 'admin@localhost'", [])
)[0][0] as Blob;
console.info("user id:", userId.blob);
const now = Date.now().toString();
const numInsertions = await execute(
`INSERT INTO post (author, title, body) VALUES (?1, 'title' , ?2)`,
[{ blob: userId.blob }, now],
);
const numDeletions = await execute(`DELETE FROM post WHERE body = ?1`, [
now,
]);
console.assert(numInsertions == numDeletions);
return "Ok";
}),
);
class Completer<T> {
public readonly promise: Promise<T>;
public complete: (value: PromiseLike<T> | T) => void;
public constructor() {
this.promise = new Promise<T>((resolve, _reject) => {
this.complete = resolve;
});
}
}
const completer = new Completer<string>();
addPeriodicCallback(100, (cancel) => {
completer.complete("resolved");
cancel();
});
addCronCallback("JS-registered Job", "@hourly", async () => {
console.info("JS-registered cron job reporting for duty 🚀");
});
addRoute(
"GET",
"/await",
stringHandler(async (_req) => await completer.promise),
);
function fibonacci(num: number): number {
switch (num) {
case 0:
return 0;
case 1:
return 1;
default:
return fibonacci(num - 1) + fibonacci(num - 2);
}
}

View File

@@ -12,10 +12,11 @@ doctest = false
name = "trail"
[features]
default = ["v8"]
default = []
# Conditionally enable "v8" feature of dep:trailbase.
v8 = ["trailbase/v8"]
swagger = ["dep:utoipa-swagger-ui"]
vendor-ssl = ["dep:openssl"]
[dependencies]
axum = { version = "^0.8.1", features=["multipart"] }
@@ -26,6 +27,7 @@ itertools = "0.14.0"
log = "^0.4.21"
mimalloc = { version = "^0.1.41", default-features = false }
minijinja = { workspace = true }
openssl = { version = "0.10.73", features = ["vendored"], optional = true }
reqwest = { workspace = true }
serde = { version = "^1.0.203", features = ["derive"] }
serde_json = "^1.0.117"

View File

@@ -1,7 +1,7 @@
//! A client library to connect to a TrailBase server via HTTP.
//!
//! TrailBase is a sub-millisecond, open-source application server with type-safe APIs, built-in
//! JS/ES6/TS runtime, realtime, auth, and admin UI built on Rust, SQLite & V8.
//! WASM runtime, realtime, auth, and admin UI built on Rust, SQLite & Wasmtime.
#![forbid(unsafe_code, clippy::unwrap_used)]
#![allow(clippy::needless_return)]

View File

@@ -24,7 +24,7 @@ name = "benchmark"
harness = false
[features]
default = ["v8", "wasm"]
default = ["wasm"]
v8 = ["dep:trailbase-js"]
wasm = ["dep:trailbase-wasm-runtime-host"]

View File

@@ -74,7 +74,8 @@ pub struct ServerOptions {
/// Limit the set of allowed origins the HTTP server will answer to.
pub cors_allowed_origins: Vec<String>,
/// Number of V8 worker threads. If set to None, default of num available cores will be used.
/// Number of dedicated runtime threads. If set to None, default of num available cores will be
/// used.
pub runtime_threads: Option<usize>,
/// TLS certificate path.

View File

@@ -24,7 +24,7 @@ regex = "1.11.0"
rusqlite = { workspace = true }
serde = { version = "^1.0.203", features = ["derive"] }
serde_json = "1.0.121"
sqlite-vec = { version = "0.1.6", default-features = false }
sqlite-vec = { workspace = true }
thiserror = "2.0.12"
trailbase-sqlean = { workspace = true }
uuid = { workspace = true }

View File

@@ -189,17 +189,14 @@ scheduling, or more generally the same CPU variability we observed earlier.
## Runtimes
[TrailBase is currently going through a transition from a V8-based runtime to a
WASM one](/blog/switching_to_a_wasm_runtime).
[TrailBase went through a transition from a V8-based runtime to a WASM one, which can support many guest languages such as JS, TS and Rust](/blog/switching_to_a_wasm_runtime).
V8's execution with just-in-time (JIT) compilation is quite speedy and is about
40x faster than Goja, PocketBase's interpreter and similar other interpreters
(the graph's y-axis is logarithmic).
With the new WASM-based runtime, execution of JS relies on bundling an
With the new WebAssembly-based runtime, execution of JS relies on bundling an
interpreter and thus performance has regressed to be roughly on-par with Goja
and other JIT-less engines.
However, guests languages that can compile natively to WASM, e.g. Rust, can shine
with roughly a 135x speed-up with strong state isolation between requests.
with roughly a 135x speed-up with strong state isolation between requests (the
graph's y-axis is logarithmic).
import { RuntimeFib40Times } from "./_benchmarks/wasm_runtime.tsx";

View File

@@ -1,25 +0,0 @@
import { addRoute, jsonHandler, parsePath, query } from "../trailbase.js";
/// Register a handler for the `/search` API route.
addRoute(
"GET",
"/search",
jsonHandler(async (req) => {
// Get the query params from the url, e.g. '/search?aroma=4&acidity=7'.
const searchParams = parsePath(req.uri).query;
const aroma = searchParams.get("aroma") ?? 8;
const flavor = searchParams.get("flavor") ?? 8;
const acid = searchParams.get("acidity") ?? 8;
const sweet = searchParams.get("sweetness") ?? 8;
// Query the database for the closest match.
return await query(
`SELECT Owner, Aroma, Flavor, Acidity, Sweetness
FROM coffee
ORDER BY vec_distance_L2(
embedding, FORMAT("[%f, %f, %f, %f]", $1, $2, $3, $4))
LIMIT 100`,
[+aroma, +flavor, +acid, +sweet],
);
}),
);

View File

@@ -1,49 +0,0 @@
import { addRoute, htmlHandler, jsonHandler, query, fs } from "../trailbase.js";
import { render } from "../../dist/server/entry-server.js";
let _template: Promise<string> | null = null;
async function getTemplate(): Promise<string> {
if (_template == null) {
const template = _template = fs.readTextFile('dist/client/index.html');
return await template;
}
return await _template;
}
addRoute(
"GET",
"/clicked",
jsonHandler(async (_req) => {
const rows = await query(
"UPDATE counter SET value = value + 1 WHERE id = 1 RETURNING value",
[],
)
const count = rows.length > 0 ? rows[0][0] as number : -1;
return { count };
}),
);
/// Register a root handler.
addRoute(
"GET",
"/",
htmlHandler(async (req) => {
// NOTE: this is replicating vite SSR template's server.js;
const rows = await query(
"SELECT value FROM counter WHERE id = 1",
[],
)
const count = rows.length > 0 ? rows[0][0] as number : 0;
const rendered = render(req.uri, count);
const html = (await getTemplate())
.replace(`<!--app-head-->`, rendered.head ?? '')
.replace(`<!--app-html-->`, rendered.html ?? '')
.replace(`<!--app-data-->`, rendered.data ?? '');
return html;
}),
);

1
vendor/sqlite-vec vendored Submodule

Submodule vendor/sqlite-vec added at 736f40e68b