fix: reduced app size by a good margin

This commit is contained in:
2026-07-20 08:44:17 +02:00
parent 7ba2699b68
commit 269e4b0e4a
21 changed files with 494 additions and 96 deletions

View File

@@ -1,4 +1,5 @@
use std::fmt::Write as _;
use std::net::IpAddr;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
@@ -72,6 +73,8 @@ pub enum Command {
#[command(subcommand)]
command: ProjectCommand,
},
/// Start the authenticated headless SSH server.
Server(ServerArgs),
/// Start interactive TUI mode (normally intercepted by the packaged launcher).
Tui,
/// Run an enabled utility Lua script from the active project.
@@ -84,6 +87,22 @@ pub enum Command {
Install,
}
#[derive(Debug, Args, Default)]
pub struct ServerArgs {
/// SSH listen address. Defaults to loopback; external access must be explicit.
#[arg(long)]
pub bind: Option<IpAddr>,
/// SSH listen port.
#[arg(long)]
pub port: Option<u16>,
/// Application database path.
#[arg(long)]
pub database: Option<PathBuf>,
/// Private application data directory containing SSH key material.
#[arg(long)]
pub data_dir: Option<PathBuf>,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum RepairPart {
PostLinks,
@@ -257,10 +276,8 @@ fn execute(command: Command, context: &RunContext, airplane: bool) -> Result<Com
if matches!(command, Command::Install) {
return install_launcher(context);
}
if matches!(command, Command::Tui) {
bail!(
"the packaged bds-cli launcher starts TUI mode directly; a direct CLI invocation cannot own the terminal"
);
if matches!(command, Command::Tui | Command::Server(_)) {
bail!("server and TUI modes are started directly by the native bds-cli entry point");
}
let db = open_database(&context.database_path)?;
@@ -277,7 +294,7 @@ fn execute(command: Command, context: &RunContext, airplane: bool) -> Result<Com
Command::Config { command } => config(&db, command),
Command::Project { command } => project(&db, command),
Command::Lua { script, args } => run_lua(&db, &script, &args, airplane),
Command::Tui | Command::Install => unreachable!(),
Command::Server(_) | Command::Tui | Command::Install => unreachable!(),
}
}
@@ -1143,6 +1160,7 @@ fn command_name(command: &Command) -> &'static str {
Command::Gallery(_) => "gallery",
Command::Config { .. } => "config",
Command::Project { .. } => "project",
Command::Server(_) => "server",
Command::Tui => "tui",
Command::Lua { .. } => "lua",
Command::Install => "install",
@@ -1223,10 +1241,24 @@ mod tests {
let help = String::from_utf8(help).unwrap();
for command in [
"rebuild", "repair", "render", "upload", "push", "pull", "post", "media", "gallery",
"config", "project", "tui", "lua", "install",
"config", "project", "server", "tui", "lua", "install",
] {
assert!(help.contains(command), "missing {command} from help");
}
let server = Cli::try_parse_from([
"bds-cli",
"server",
"--bind",
"127.0.0.2",
"--port",
"2233",
"--database",
"/tmp/bds.db",
"--data-dir",
"/tmp/bds",
])
.unwrap();
assert!(matches!(server.command, Command::Server(_)));
assert!(Cli::try_parse_from(["bds-cli", "unknown"]).is_err());
assert!(Cli::try_parse_from(["bds-cli", "render", "--incremental", "--force"]).is_err());
assert!(Cli::try_parse_from(["bds-cli", "repair", "unknown"]).is_err());
@@ -1511,9 +1543,11 @@ mod tests {
)
.unwrap();
let target = fixture.home_dir.join(".local/bin/bds-cli");
assert_eq!(
target.canonicalize().unwrap(),
executable.canonicalize().unwrap()
assert!(!target.is_symlink());
assert!(
std::fs::read_to_string(target)
.unwrap()
.contains(executable.canonicalize().unwrap().to_str().unwrap())
);
}
}

View File

@@ -20,6 +20,33 @@ fn main() -> ExitCode {
}
};
if let bds_cli::Command::Server(args) = &cli.command {
let data_root = args
.data_dir
.clone()
.unwrap_or_else(bds_core::util::application_data_dir);
let database_path = args
.database
.clone()
.unwrap_or_else(|| data_root.join("bds.db"));
let config = match bds_server::ServerConfig::from_environment(database_path, data_root) {
Ok(mut config) => {
if let Some(bind) = args.bind {
config.bind = bind;
}
if let Some(port) = args.port {
config.port = port;
}
config
}
Err(error) => {
eprintln!("Error: {error:#}");
return ExitCode::from(1);
}
};
return run_headless(bds_server::boot::BootMode::Server, config);
}
if matches!(cli.command, bds_cli::Command::Tui) {
let data_root = bds_core::util::application_data_dir();
let config = match bds_server::ServerConfig::from_environment(
@@ -32,13 +59,7 @@ fn main() -> ExitCode {
return ExitCode::from(1);
}
};
return match bds_server::run_headless(bds_server::boot::BootMode::Tui, config) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("Error: {error:#}");
ExitCode::from(1)
}
};
return run_headless(bds_server::boot::BootMode::Tui, config);
}
let json = cli.json;
@@ -71,3 +92,13 @@ fn main() -> ExitCode {
}
}
}
fn run_headless(mode: bds_server::boot::BootMode, config: bds_server::ServerConfig) -> ExitCode {
match bds_server::run_headless(mode, config) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("Error: {error:#}");
ExitCode::from(1)
}
}
}

View File

@@ -4,6 +4,9 @@ edition.workspace = true
version.workspace = true
license.workspace = true
[lib]
crate-type = ["dylib"]
[dependencies]
uuid = { workspace = true }
serde = { workspace = true }

View File

@@ -14,14 +14,18 @@ pub fn install_launcher(executable: &Path, home_dir: &Path) -> EngineResult<Path
let bin_dir = home_dir.join(".local/bin");
std::fs::create_dir_all(&bin_dir)?;
let target = bin_dir.join(if cfg!(windows) {
"bds-cli.exe"
"bds-cli.cmd"
} else {
"bds-cli"
});
let source = executable.canonicalize()?;
let launcher = launcher_contents(&source);
#[cfg(unix)]
if target.is_symlink() && target.canonicalize().ok().as_ref() == Some(&source) {
std::fs::remove_file(&target)?;
}
if target.exists() {
let existing = target.canonicalize().ok();
let source = executable.canonicalize()?;
if existing.as_ref() != Some(&source) {
if std::fs::read(&target).ok().as_deref() != Some(launcher.as_bytes()) {
return Err(EngineError::Conflict(format!(
"refusing to overwrite existing launcher at {}",
target.display()
@@ -30,13 +34,29 @@ pub fn install_launcher(executable: &Path, home_dir: &Path) -> EngineResult<Path
return Ok(target);
}
std::fs::write(&target, launcher)?;
#[cfg(unix)]
std::os::unix::fs::symlink(executable.canonicalize()?, &target)?;
#[cfg(windows)]
std::fs::copy(executable, &target)?;
{
use std::os::unix::fs::PermissionsExt as _;
let mut permissions = std::fs::metadata(&target)?.permissions();
permissions.set_mode(0o755);
std::fs::set_permissions(&target, permissions)?;
}
Ok(target)
}
#[cfg(unix)]
fn launcher_contents(executable: &Path) -> String {
let quoted = executable.to_string_lossy().replace('\'', "'\"'\"'");
format!("#!/bin/sh\nexec '{quoted}' \"$@\"\n")
}
#[cfg(windows)]
fn launcher_contents(executable: &Path) -> String {
let escaped = executable.to_string_lossy().replace('%', "%%");
format!("@echo off\r\n\"{escaped}\" %*\r\n")
}
/// Resolve the CLI shipped beside the desktop executable and install it.
pub fn install_packaged_launcher(home_dir: &Path) -> EngineResult<PathBuf> {
let app = std::env::current_exe()?;
@@ -59,10 +79,9 @@ mod tests {
std::fs::write(&executable, b"binary").unwrap();
let home = root.path().join("home");
let target = install_launcher(&executable, &home).unwrap();
assert_eq!(
target.canonicalize().unwrap(),
executable.canonicalize().unwrap()
);
assert!(!target.is_symlink());
let launcher = std::fs::read_to_string(&target).unwrap();
assert!(launcher.contains(executable.canonicalize().unwrap().to_str().unwrap()));
assert_eq!(install_launcher(&executable, &home).unwrap(), target);
std::fs::remove_file(&target).unwrap();
@@ -70,4 +89,24 @@ mod tests {
assert!(install_launcher(&executable, &home).is_err());
assert_eq!(std::fs::read(&target).unwrap(), b"mine");
}
#[cfg(unix)]
#[test]
fn replaces_the_previous_installer_symlink_with_a_forwarding_launcher() {
let root = tempfile::tempdir().unwrap();
let executable = root.path().join("packaged-bds-cli");
std::fs::write(&executable, b"binary").unwrap();
let home = root.path().join("home");
let target = home.join(".local/bin/bds-cli");
std::fs::create_dir_all(target.parent().unwrap()).unwrap();
std::os::unix::fs::symlink(&executable, &target).unwrap();
assert_eq!(install_launcher(&executable, &home).unwrap(), target);
assert!(!target.is_symlink());
assert!(
std::fs::read_to_string(target)
.unwrap()
.starts_with("#!/bin/sh")
);
}
}

View File

@@ -1599,7 +1599,7 @@ mod tests {
let engine = GitEngine::with_executable_and_timeouts(
dir.path(),
executable,
Duration::from_secs(1),
Duration::from_secs(5),
Duration::from_secs(1),
);

View File

@@ -0,0 +1,6 @@
[package]
name = "bds-package-runtime"
edition.workspace = true
version.workspace = true
license.workspace = true

View File

@@ -0,0 +1,231 @@
use std::{
env,
error::Error,
ffi::OsStr,
fs,
path::{Path, PathBuf},
process::Command,
};
fn main() -> Result<(), Box<dyn Error>> {
let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
if env::args_os().nth(1).as_deref() == Some(OsStr::new("sign-app-for-dmg")) {
return sign_app_for_dmg(&workspace);
}
run(
Command::new(env::var_os("CARGO").unwrap_or_else(|| "cargo".into()))
.current_dir(&workspace)
.args([
"build",
"--release",
"-p",
"bds-ui",
"-p",
"bds-cli",
"-p",
"bds-mcp",
]),
"release build",
)?;
let target = env::var_os("CARGO_TARGET_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| workspace.join("target"));
let release = target.join("release");
let staging = release.join("bds-runtime");
if staging.exists() {
fs::remove_dir_all(&staging)?;
}
fs::create_dir_all(&staging)?;
let shared_libraries = shared_library_names().map(|name| release.join(name));
for library in &shared_libraries {
copy_runtime(library, &staging)?;
}
let rustc = env::var_os("RUSTC").unwrap_or_else(|| "rustc".into());
let output = Command::new(rustc)
.args(["--print", "target-libdir"])
.output()?;
if !output.status.success() {
return Err("rustc --print target-libdir failed".into());
}
let rust_lib_dir = PathBuf::from(String::from_utf8(output.stdout)?.trim());
let mut std_libraries = fs::read_dir(&rust_lib_dir)?
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| is_dynamic_std(path))
.collect::<Vec<_>>();
std_libraries.sort();
if std_libraries.is_empty() {
return Err(format!(
"no dynamic Rust standard library found in {}",
rust_lib_dir.display()
)
.into());
}
for library in &std_libraries {
copy_runtime(&library, &staging)?;
}
#[cfg(target_os = "macos")]
rewrite_macos_install_names(&release, &staging, &shared_libraries, &std_libraries)?;
Ok(())
}
fn sign_app_for_dmg(workspace: &Path) -> Result<(), Box<dyn Error>> {
#[cfg(target_os = "macos")]
if env::var_os("CARGO_PACKAGER_FORMAT").as_deref() == Some(OsStr::new("dmg")) {
let target = env::var_os("CARGO_TARGET_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| workspace.join("target"));
run(
Command::new("codesign")
.args(["--force", "--sign", "-"])
.arg(target.join("release/Blogging Desktop Server.app")),
"macOS app ad-hoc signing",
)?;
}
Ok(())
}
fn run(command: &mut Command, description: &str) -> Result<(), Box<dyn Error>> {
if command.status()?.success() {
Ok(())
} else {
Err(format!("{description} failed").into())
}
}
fn copy_runtime(source: &Path, staging: &Path) -> Result<(), Box<dyn Error>> {
let name = source
.file_name()
.ok_or_else(|| format!("runtime path has no filename: {}", source.display()))?;
let destination = staging.join(name);
fs::copy(source, &destination)?;
let mut permissions = fs::metadata(&destination)?.permissions();
permissions.set_readonly(false);
fs::set_permissions(&destination, permissions)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
fs::set_permissions(&destination, fs::Permissions::from_mode(0o755))?;
}
Ok(())
}
fn shared_library_names() -> [&'static str; 2] {
if cfg!(target_os = "windows") {
["bds_core.dll", "bds_server.dll"]
} else if cfg!(target_os = "macos") {
["libbds_core.dylib", "libbds_server.dylib"]
} else {
["libbds_core.so", "libbds_server.so"]
}
}
fn is_dynamic_std(path: &Path) -> bool {
let name = path.file_name().and_then(OsStr::to_str).unwrap_or_default();
if cfg!(target_os = "windows") {
name.starts_with("std-") && name.ends_with(".dll")
} else if cfg!(target_os = "macos") {
name.starts_with("libstd-") && name.ends_with(".dylib")
} else {
name.starts_with("libstd-") && name.ends_with(".so")
}
}
#[cfg(target_os = "macos")]
fn rewrite_macos_install_names(
release: &Path,
staging: &Path,
shared_libraries: &[PathBuf],
std_libraries: &[PathBuf],
) -> Result<(), Box<dyn Error>> {
let mut replacements = Vec::new();
for source in shared_libraries.iter().chain(std_libraries) {
let old_name = macho_install_name(source)?;
let file_name = source
.file_name()
.and_then(OsStr::to_str)
.ok_or_else(|| format!("invalid dylib filename: {}", source.display()))?;
replacements.push((old_name, format!("@rpath/{file_name}")));
}
let mut targets = ["bds-ui", "bds-cli", "bds-mcp"]
.map(|name| release.join(name))
.into_iter()
.collect::<Vec<_>>();
targets.extend(
shared_libraries
.iter()
.filter_map(|source| source.file_name().map(|name| staging.join(name))),
);
targets.extend(
std_libraries
.iter()
.filter_map(|source| source.file_name().map(|name| staging.join(name))),
);
for target in &targets {
for (old_name, new_name) in &replacements {
run(
Command::new("install_name_tool")
.args(["-change", old_name, new_name])
.arg(target),
"Mach-O dependency rewrite",
)?;
}
}
for (_, new_name) in &replacements {
let file_name = new_name.trim_start_matches("@rpath/");
run(
Command::new("install_name_tool")
.args(["-id", new_name])
.arg(staging.join(file_name)),
"Mach-O install-name rewrite",
)?;
}
for target in targets {
run(
Command::new("codesign")
.args(["--force", "--sign", "-"])
.arg(target),
"Mach-O ad-hoc signing",
)?;
}
Ok(())
}
#[cfg(target_os = "macos")]
fn macho_install_name(library: &Path) -> Result<String, Box<dyn Error>> {
let output = Command::new("otool").arg("-D").arg(library).output()?;
if !output.status.success() {
return Err(format!("otool -D failed for {}", library.display()).into());
}
String::from_utf8(output.stdout)?
.lines()
.nth(1)
.map(str::to_owned)
.ok_or_else(|| format!("no Mach-O install name in {}", library.display()).into())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn recognizes_only_the_platform_dynamic_standard_library() {
let valid = if cfg!(target_os = "windows") {
"std-123.dll"
} else if cfg!(target_os = "macos") {
"libstd-123.dylib"
} else {
"libstd-123.so"
};
assert!(is_dynamic_std(Path::new(valid)));
assert!(!is_dynamic_std(Path::new("libstd-123.rlib")));
for library in shared_library_names() {
assert!(!is_dynamic_std(Path::new(library)));
}
}
}

View File

@@ -4,6 +4,9 @@ edition.workspace = true
version.workspace = true
license.workspace = true
[lib]
crate-type = ["dylib"]
[dependencies]
bds-core = { workspace = true }
bds-editor = { workspace = true }

View File

@@ -0,0 +1,9 @@
fn main() {
// syntect and fastembed both reach onig_sys across separate Rust dylibs.
// Keep the vendored static archive on this dylib's final native link line.
if std::env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("msvc") {
println!("cargo:rustc-link-arg=onig.lib");
} else {
println!("cargo:rustc-link-arg=-lonig");
}
}

View File

@@ -1,41 +0,0 @@
use std::net::IpAddr;
use std::path::PathBuf;
use bds_server::boot::BootMode;
use clap::Parser;
#[derive(Debug, Parser)]
#[command(
name = "bds-server",
about = "Headless RuDS engine host over authenticated SSH"
)]
struct Args {
/// SSH listen address. Defaults to loopback; external access must be explicit.
#[arg(long)]
bind: Option<IpAddr>,
/// SSH listen port.
#[arg(long)]
port: Option<u16>,
/// Application database path.
#[arg(long)]
database: Option<PathBuf>,
/// Private application data directory containing SSH key material.
#[arg(long)]
data_dir: Option<PathBuf>,
}
fn main() -> anyhow::Result<()> {
let args = Args::parse();
let data_root = args
.data_dir
.unwrap_or_else(bds_core::util::application_data_dir);
let database_path = args.database.unwrap_or_else(|| data_root.join("bds.db"));
let mut config = bds_server::ServerConfig::from_environment(database_path, data_root)?;
if let Some(bind) = args.bind {
config.bind = bind;
}
if let Some(port) = args.port {
config.port = port;
}
bds_server::run_headless(BootMode::Server, config)
}

View File

@@ -39,12 +39,15 @@ winresource = "0.1"
product-name = "Blogging Desktop Server"
identifier = "de.rfc1437.ruds"
description = "A desktop application for writing and publishing static blogs."
before-packaging-command = "cargo build --release -p bds-ui -p bds-cli -p bds-mcp -p bds-server"
before-packaging-command = "cargo run --quiet -p bds-package-runtime"
before-each-package-command = "cargo run --quiet -p bds-package-runtime -- sign-app-for-dmg"
binaries = [
{ path = "bds-ui", main = true },
{ path = "bds-cli" },
{ path = "bds-mcp" },
{ path = "bds-server" },
]
resources = [
{ src = "../../target/release/bds-runtime/*", target = "." },
]
deep-link-protocols = [{ schemes = ["ruds"] }]
icons = [
@@ -54,8 +57,7 @@ icons = [
]
[package.metadata.packager.macos]
minimum-system-version = "13.0"
signing-identity = "-"
minimum-system-version = "26.0"
[package.metadata.packager.nsis]
installer-icon = "assets/app-icons/bds.ico"

View File

@@ -1,6 +1,12 @@
fn main() {
println!("cargo:rerun-if-changed=assets/app-icons/bds.ico");
// Wry and bds-core both reach objc2's native exception helper across
// separate Rust dylibs, so retain the static helper on the UI link line.
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") {
println!("cargo:rustc-link-arg=-lobjc2_exception_helper_0_1");
}
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") {
winresource::WindowsResource::new()
.set_icon("assets/app-icons/bds.ico")

View File

@@ -24,22 +24,55 @@ fn desktop_packages_have_native_icons_and_cargo_commands() {
"assets/app-icons/bds.icns",
"identifier = \"de.rfc1437.ruds\"",
"deep-link-protocols = [{ schemes = [\"ruds\"] }]",
"signing-identity = \"-\"",
"cargo build --release -p bds-ui -p bds-cli -p bds-mcp",
"minimum-system-version = \"26.0\"",
"../../target/release/bds-runtime/*",
"cargo run --quiet -p bds-package-runtime",
"cargo run --quiet -p bds-package-runtime -- sign-app-for-dmg",
"{ path = \"bds-mcp\" }",
] {
assert!(manifest.contains(required), "missing {required}");
}
assert!(!manifest.contains("signing-identity"));
let cargo_config = fs::read_to_string(Path::new(CRATE_DIR).join("../../.cargo/config.toml"))
.expect("Cargo packaging aliases");
for alias in ["bundle-macos", "bundle-windows", "bundle-linux"] {
assert!(cargo_config.contains(alias), "missing cargo {alias}");
}
assert!(cargo_config.contains("prefer-dynamic"));
assert!(cargo_config.contains("-mmacosx-version-min=26.0"));
assert!(cargo_config.contains("@executable_path/../Resources"));
assert!(cargo_config.contains("$ORIGIN/../lib/bds-ui"));
let main = fs::read_to_string(Path::new(CRATE_DIR).join("src/main.rs")).unwrap();
assert!(main.contains("assets/app-icons/bds.png"));
let build = fs::read_to_string(Path::new(CRATE_DIR).join("build.rs")).unwrap();
assert!(build.contains("set_icon(\"assets/app-icons/bds.ico\")"));
let workspace_manifest =
fs::read_to_string(Path::new(CRATE_DIR).join("../../Cargo.toml")).unwrap();
assert!(workspace_manifest.contains("strip = \"symbols\""));
assert!(workspace_manifest.contains("ort-download-binaries-rustls-tls"));
let core_manifest =
fs::read_to_string(Path::new(CRATE_DIR).join("../bds-core/Cargo.toml")).unwrap();
assert!(core_manifest.contains("crate-type = [\"dylib\"]"));
let server_manifest =
fs::read_to_string(Path::new(CRATE_DIR).join("../bds-server/Cargo.toml")).unwrap();
assert!(server_manifest.contains("crate-type = [\"dylib\"]"));
assert!(!manifest.contains("{ path = \"bds-server\" }"));
assert!(!manifest.contains("-p bds-server"));
assert!(
!Path::new(CRATE_DIR)
.join("../bds-server/src/main.rs")
.exists()
);
let packager =
fs::read_to_string(Path::new(CRATE_DIR).join("../bds-package-runtime/src/main.rs"))
.unwrap();
assert!(packager.contains("Command::new(\"codesign\")"));
assert!(packager.contains("CARGO_PACKAGER_FORMAT"));
assert!(!packager.contains("--options"));
}