chore(chat-cli): switch transport at runtime via --transport flag

Both file and logos-delivery transports are now compiled into a single
binary and selected at runtime (default: logos-delivery), replacing the
env-var-driven build-time cfg.
This commit is contained in:
osmaczko 2026-04-27 14:23:15 +02:00
parent eaeffcd21f
commit 9e5f5573cb
No known key found for this signature in database
GPG Key ID: 6A385380FD275B44
8 changed files with 103 additions and 140 deletions

View File

@ -16,8 +16,11 @@ jobs:
steps:
- uses: actions/checkout@v4
- run: rustup update stable && rustup default stable
- run: cargo build --verbose
- run: cargo test --verbose
# chat-cli's build.rs unconditionally links liblogosdelivery and requires
# LOGOS_DELIVERY_LIB_DIR. The smoketest job builds and exercises it under
# Nix; here we keep the toolchain-only job fast by skipping it.
- run: cargo build --verbose --workspace --exclude chat-cli
- run: cargo test --verbose --workspace --exclude chat-cli
clippy:
name: Clippy
@ -26,7 +29,7 @@ jobs:
- uses: actions/checkout@v4
- run: rustup update stable && rustup default stable
- run: rustup component add clippy
- run: cargo clippy --all-targets --all-features -- -D warnings
- run: cargo clippy --all-targets --all-features --workspace --exclude chat-cli -- -D warnings
fmt:
name: Format

View File

@ -4,16 +4,17 @@ Supporting library for Logos-chat
## Example app
[`bin/chat-cli`](bin/chat-cli/) is an end-to-end encrypted CLI chat app
built on this library. It uses [logos-delivery](https://github.com/logos-messaging/logos-delivery)
built on this library. By default it uses [logos-delivery](https://github.com/logos-messaging/logos-delivery)
(Waku-based) as the transport so two users anywhere in the world can chat by
sharing an intro bundle.
sharing an intro bundle. A local file transport is also bundled in; pick at
runtime with `--transport <logos-delivery|file>`.
```sh
# Build logos-delivery with Nix
nix build .#logos-delivery
# Build chat-cli with Cargo
LOGOS_DELIVERY_LIB_DIR=./result/lib cargo build --release -p chat-cli
# Run binary
# Run binary (defaults to --transport logos-delivery)
./target/release/chat-cli --name alice
```

View File

@ -21,6 +21,3 @@ base64 = "0.22"
thiserror = "2"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(logos_delivery)'] }

View File

@ -4,8 +4,6 @@ A terminal chat application built on top of libchat. End-to-end encrypted messag
## Building
### With logos-delivery transport (recommended)
[logos-delivery](https://github.com/logos-messaging/logos-delivery) is exposed as a Nix package.
Build it once, then point `LOGOS_DELIVERY_LIB_DIR` at the result:
@ -16,31 +14,35 @@ LOGOS_DELIVERY_LIB_DIR=./result/lib cargo build --release -p chat-cli
The binary lands at `target/release/chat-cli`.
### File transport only (no Nix required)
```bash
cargo build --release -p chat-cli
```
## Transports
| Transport | Description |
|-----------|-------------|
| File (default) | Shared directory; no network needed — great for local testing |
| logos-delivery | Embedded Waku node on the logos.dev network |
Both transports are compiled into the binary and selected at runtime via `--transport`:
The transport is selected automatically at compile time: if `LOGOS_DELIVERY_LIB_DIR` is set when building, logos-delivery is used; otherwise the file transport is used.
| Value (`--transport`) | Description |
|-----------------------|-------------|
| `logos-delivery` (default) | Embedded Waku node on the logos.dev network |
| `file` | Shared directory; no network needed — great for local testing |
## Quick start (file transport)
## Quick start
Run two instances in separate terminals, pointing at the same data directory:
Run two instances in separate terminals:
```bash
# Terminal 1
cargo run -p chat-cli -- --name alice
cargo run -p chat-cli -- --name alice --port 60001
# Terminal 2
cargo run -p chat-cli -- --name bob
cargo run -p chat-cli -- --name bob --port 60002
```
For local-only testing without any network dependency, use the file transport:
```bash
# Terminal 1
cargo run -p chat-cli -- --name alice --transport file
# Terminal 2
cargo run -p chat-cli -- --name bob --transport file
```
### Establishing a connection
@ -49,20 +51,14 @@ cargo run -p chat-cli -- --name bob
2. In Bob's terminal, type `/connect <paste bundle here>`.
3. Bob's "Hello!" message appears in Alice's terminal. Both can now chat.
## logos-delivery transport
After building with `LOGOS_DELIVERY_LIB_DIR` set, run:
```bash
./target/release/chat-cli --name alice
```
Optional flags:
## Options
| Flag | Default | Description |
|------|---------|-------------|
| `--db <path>` | *(ephemeral)* | SQLite file for persistent identity across restarts |
| `--preset <name>` | `logos.dev` | Network preset (`logos.dev` or `twn`) |
| `--transport <kind>` | `logos-delivery` | Transport to use (`logos-delivery` or `file`) |
| `--data <dir>` | `tmp/chat-cli-data` | Data directory (UI state and default SQLite path) |
| `--db <path>` | `<data>/<name>.db` | SQLite file for persistent identity |
| `--preset <name>` | `logos.dev` | logos-delivery network preset |
| `--port <n>` | `60000` | TCP port for the embedded logos-delivery node |
| `--log-file <path>` | *(stderr, off)* | Write logs to a file instead of stderr |
@ -80,7 +76,7 @@ Optional flags:
| `/clear` | Clear current chat's message history |
| `/quit` · `Esc` · `Ctrl+C` | Exit |
## Storage (file transport)
## Storage
All data lives under `tmp/chat-cli-data/` by default (override with `--data`):
@ -88,7 +84,7 @@ All data lives under `tmp/chat-cli-data/` by default (override with `--data`):
|------|----------|
| `<name>.db` | SQLite — identity keys, ratchet state, chat metadata (encrypted) |
| `<name>_state.json` | UI state — message history, active chat |
| `transport/<name>/` | Inbox directory watched for incoming messages |
| `transport/<name>/` | Inbox directory watched for incoming messages (file transport only) |
The SQLite database can be inspected with *DB Browser for SQLite*: password `chat-cli`, cipher `SQLCipher 4 defaults`.
@ -97,7 +93,7 @@ The SQLite database can be inspected with *DB Browser for SQLite*: password `cha
```
bin/chat-cli/
├── src/
│ ├── main.rs entry point, CLI arg parsing, transport selection
│ ├── main.rs entry point, CLI arg parsing, runtime transport dispatch
│ ├── app.rs application state and command handling
│ ├── ui.rs ratatui terminal UI
│ ├── utils.rs shared helpers
@ -105,5 +101,5 @@ bin/chat-cli/
│ └── transport/
│ ├── file.rs file-based transport
│ └── logos_delivery.rs logos-delivery (Waku) transport + FFI
└── build.rs links liblogosdelivery when LOGOS_DELIVERY_LIB_DIR is set
└── build.rs links liblogosdelivery (LOGOS_DELIVERY_LIB_DIR required)
```

View File

@ -1,20 +1,17 @@
fn main() {
println!("cargo::rustc-check-cfg=cfg(logos_delivery)");
println!("cargo:rerun-if-env-changed=LOGOS_DELIVERY_LIB_DIR");
let Ok(lib_dir) = std::env::var("LOGOS_DELIVERY_LIB_DIR") else {
return;
};
let lib_dir = std::env::var("LOGOS_DELIVERY_LIB_DIR").expect(
"LOGOS_DELIVERY_LIB_DIR must be set; build liblogosdelivery via \
`nix build .#logos-delivery` and point this var at the result/lib directory",
);
println!("cargo:rustc-cfg=logos_delivery");
println!("cargo:rustc-link-search=native={lib_dir}");
println!("cargo:rustc-link-lib=dylib=logosdelivery");
// Set rpath so the binary finds the shared library at runtime.
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
match target_os.as_str() {
"macos" => println!("cargo:rustc-link-arg=-Wl,-rpath,{lib_dir}"),
"linux" => println!("cargo:rustc-link-arg=-Wl,-rpath,{lib_dir}"),
"macos" | "linux" => println!("cargo:rustc-link-arg=-Wl,-rpath,{lib_dir}"),
other => panic!("unsupported OS for logos-delivery transport: {other}"),
}
}

View File

@ -3,14 +3,22 @@ mod transport;
mod ui;
mod utils;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::sync::mpsc;
use anyhow::{Context, Result};
use clap::Parser;
use clap::{Parser, ValueEnum};
use client::DeliveryService;
use app::ChatApp;
#[derive(Copy, Clone, Debug, ValueEnum)]
#[value(rename_all = "kebab-case")]
enum TransportKind {
File,
LogosDelivery,
}
#[derive(Parser, Debug)]
#[command(name = "chat-cli", about = "End-to-end encrypted terminal chat")]
struct Cli {
@ -18,17 +26,20 @@ struct Cli {
#[arg(long, short)]
name: String,
// ── File-transport options ────────────────────────────────────────────────
/// Shared data directory for file transport (both peers must use the same path).
/// Which delivery transport to use.
#[arg(long, value_enum, default_value_t = TransportKind::LogosDelivery)]
transport: TransportKind,
/// Data directory (used for UI state and the default SQLite path).
#[arg(long, default_value = "tmp/chat-cli-data")]
data: PathBuf,
// ── logos-delivery transport options ──────────────────────────────────────
/// Persistent SQLite database for logos-delivery transport (omit for ephemeral identity).
/// Override the SQLite database path (defaults to `<data>/<name>.db`).
#[arg(long)]
db: Option<PathBuf>,
/// logos-delivery network preset (`logos.dev` or `twn`).
// ── logos-delivery transport options ──────────────────────────────────────
/// logos-delivery network preset (e.g. `logos.dev`).
#[arg(long, default_value = "logos.dev")]
preset: String,
@ -48,30 +59,54 @@ struct Cli {
fn main() -> Result<()> {
let cli = Cli::parse();
setup_logging(cli.log_file.as_deref())?;
#[cfg(logos_delivery)]
return run_logos_delivery(cli);
#[cfg(not(logos_delivery))]
run_file(cli)
}
#[cfg(not(logos_delivery))]
fn run_file(cli: Cli) -> Result<()> {
use transport::file::FileTransport;
std::fs::create_dir_all(&cli.data).context("failed to create data directory")?;
println!("Starting chat as '{}'...", cli.name);
println!("Data dir: {}", cli.data.display());
match cli.transport {
TransportKind::File => {
let transport_dir = cli.data.join("transport");
let (transport, inbound) = transport::file::FileTransport::new(&transport_dir)
.context("failed to create file transport")?;
run(transport, inbound, &cli)
}
TransportKind::LogosDelivery => {
use transport::logos_delivery::{Config, Service};
let transport_dir = cli.data.join("transport");
let (transport, inbound) =
FileTransport::new(&transport_dir).context("failed to create file transport")?;
eprintln!("Starting logos-delivery node (preset={})...", cli.preset);
eprintln!("This may take a few seconds while connecting to the network.");
let cfg = Config {
preset: cli.preset.clone(),
tcp_port: cli.port,
..Default::default()
};
let (transport, inbound) =
Service::start(cfg).context("failed to start logos-delivery")?;
eprintln!("Node connected. Initializing chat client...");
run(transport, inbound, &cli)
}
}
}
fn run<D: DeliveryService>(
transport: D,
inbound: mpsc::Receiver<Vec<u8>>,
cli: &Cli,
) -> Result<()> {
let db_path = cli
.db
.clone()
.unwrap_or_else(|| cli.data.join(format!("{}.db", cli.name)));
let db_str = db_path
.to_str()
.context("db path contains non-UTF-8 characters")?
.to_string();
let db_path = cli.data.join(format!("{}.db", cli.name));
let client = client::ChatClient::open(
cli.name.clone(),
client::StorageConfig::Encrypted {
path: db_path.to_string_lossy().to_string(),
path: db_str,
key: "chat-cli".to_string(),
},
transport,
@ -91,71 +126,6 @@ fn run_file(cli: Cli) -> Result<()> {
result
}
#[cfg_attr(not(logos_delivery), allow(dead_code, unused_variables))]
fn run_logos_delivery(cli: Cli) -> Result<()> {
#[cfg(logos_delivery)]
{
use transport::logos_delivery::{Config, Service};
eprintln!("Starting logos-delivery node (preset={})...", cli.preset);
eprintln!("This may take a few seconds while connecting to the network.");
let logos_cfg = Config {
preset: cli.preset.clone(),
tcp_port: cli.port,
..Default::default()
};
let (delivery, inbound) =
Service::start(logos_cfg).context("failed to start logos-delivery")?;
eprintln!("Node connected. Initializing chat client...");
let data_dir = cli
.db
.as_ref()
.and_then(|p| p.parent())
.map(|p| p.to_path_buf())
.unwrap_or_else(|| cli.data.clone());
let client = match cli.db {
Some(ref path) => {
let db_str = path
.to_str()
.context("db path contains non-UTF-8 characters")?
.to_string();
client::ChatClient::open(
cli.name.clone(),
client::StorageConfig::Encrypted {
path: db_str,
key: "chat-cli".to_string(),
},
delivery,
)
.map_err(|e| anyhow::anyhow!("{e:?}"))
.context("failed to open persistent client")?
}
None => client::ChatClient::new(cli.name.clone(), delivery),
};
let mut app = ChatApp::new(client, inbound, &cli.name, &data_dir)?;
if cli.smoketest {
return Ok(());
}
let mut terminal = ui::init().context("failed to initialize terminal")?;
let result = run_app(&mut terminal, &mut app);
ui::restore().context("failed to restore terminal")?;
return result;
}
#[cfg(not(logos_delivery))]
anyhow::bail!(
"logos-delivery transport is not available in this build.\n\
Build with LOGOS_DELIVERY_LIB_DIR set to enable it."
)
}
fn run_app<D: DeliveryService>(terminal: &mut ui::Tui, app: &mut ChatApp<D>) -> Result<()> {
loop {
app.process_incoming()?;
@ -167,7 +137,7 @@ fn run_app<D: DeliveryService>(terminal: &mut ui::Tui, app: &mut ChatApp<D>) ->
Ok(())
}
fn setup_logging(log_file: Option<&std::path::Path>) -> Result<()> {
fn setup_logging(log_file: Option<&Path>) -> Result<()> {
use tracing_subscriber::EnvFilter;
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("warn"));

View File

@ -1,4 +1,2 @@
#[cfg(not(logos_delivery))]
pub mod file;
#[cfg(logos_delivery)]
pub mod logos_delivery;

View File

@ -57,6 +57,7 @@
nativeBuildInputs = [ pkgs.perl pkgs.pkg-config pkgs.cmake ];
buildType = "release";
doCheck = false;
cargoBuildFlags = [ "--workspace" "--exclude" "chat-cli" ];
postBuild = ''
cargo run --frozen --release --bin generate-headers --features headers -p client-ffi -- crates/client-ffi/client_ffi.h