feat: file-based audio testing + Hetzner build scripts
CLI modes: - --send-tone <secs>: send 440Hz test tone (no mic needed) - --record <file.raw>: save received audio to raw PCM file - --help: usage info - Combine: --send-tone 10 --record out.raw Raw PCM format: 48kHz mono s16le Play with: ffplay -f s16le -ar 48000 -ac 1 out.raw Build scripts: - scripts/build-linux.sh: Hetzner VPS build with auto-cleanup - scripts/cleanup-builder.sh: kill stale builders Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,64 +1,154 @@
|
|||||||
//! WarzonePhone CLI test client.
|
//! WarzonePhone CLI test client.
|
||||||
//!
|
//!
|
||||||
//! Usage: wzp-client [--live] [relay-addr]
|
//! Usage:
|
||||||
|
//! wzp-client [relay-addr] Send silence frames (connectivity test)
|
||||||
|
//! wzp-client --live [relay-addr] Live mic/speaker mode
|
||||||
|
//! wzp-client --send-tone 10 [relay-addr] Send 10s of 440Hz test tone
|
||||||
|
//! wzp-client --record out.raw [relay-addr] Record received audio to raw PCM file
|
||||||
|
//! wzp-client --send-tone 10 --record out.raw [relay-addr] Both at once
|
||||||
//!
|
//!
|
||||||
//! Without `--live`: sends silence frames for testing.
|
//! Raw PCM files are 48kHz mono 16-bit signed little-endian.
|
||||||
//! With `--live`: captures microphone audio and plays received audio through speakers.
|
//! Play with: ffplay -f s16le -ar 48000 -ac 1 out.raw
|
||||||
|
//! Or convert: ffmpeg -f s16le -ar 48000 -ac 1 -i out.raw out.wav
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
use wzp_client::audio_io::{AudioCapture, AudioPlayback, FRAME_SAMPLES};
|
|
||||||
use wzp_client::call::{CallConfig, CallDecoder, CallEncoder};
|
use wzp_client::call::{CallConfig, CallDecoder, CallEncoder};
|
||||||
use wzp_proto::MediaTransport;
|
use wzp_proto::MediaTransport;
|
||||||
|
|
||||||
|
const FRAME_SAMPLES: usize = 960; // 20ms @ 48kHz
|
||||||
|
|
||||||
|
/// Generate a sine wave tone.
|
||||||
|
fn generate_sine_frame(freq_hz: f32, sample_rate: u32, frame_offset: u64) -> Vec<i16> {
|
||||||
|
let start_sample = frame_offset * FRAME_SAMPLES as u64;
|
||||||
|
(0..FRAME_SAMPLES)
|
||||||
|
.map(|i| {
|
||||||
|
let t = (start_sample + i as u64) as f32 / sample_rate as f32;
|
||||||
|
(f32::sin(2.0 * std::f32::consts::PI * freq_hz * t) * 16000.0) as i16
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct CliArgs {
|
||||||
|
relay_addr: SocketAddr,
|
||||||
|
live: bool,
|
||||||
|
send_tone_secs: Option<u32>,
|
||||||
|
record_file: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_args() -> CliArgs {
|
||||||
|
let args: Vec<String> = std::env::args().collect();
|
||||||
|
let mut live = false;
|
||||||
|
let mut send_tone_secs = None;
|
||||||
|
let mut record_file = None;
|
||||||
|
let mut relay_str = None;
|
||||||
|
|
||||||
|
let mut i = 1;
|
||||||
|
while i < args.len() {
|
||||||
|
match args[i].as_str() {
|
||||||
|
"--live" => live = true,
|
||||||
|
"--send-tone" => {
|
||||||
|
i += 1;
|
||||||
|
send_tone_secs = Some(
|
||||||
|
args.get(i)
|
||||||
|
.expect("--send-tone requires seconds")
|
||||||
|
.parse()
|
||||||
|
.expect("--send-tone value must be a number"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"--record" => {
|
||||||
|
i += 1;
|
||||||
|
record_file = Some(
|
||||||
|
args.get(i)
|
||||||
|
.expect("--record requires a filename")
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"--help" | "-h" => {
|
||||||
|
eprintln!("Usage: wzp-client [options] [relay-addr]");
|
||||||
|
eprintln!();
|
||||||
|
eprintln!("Options:");
|
||||||
|
eprintln!(" --live Live mic/speaker mode");
|
||||||
|
eprintln!(" --send-tone <secs> Send a 440Hz test tone for N seconds");
|
||||||
|
eprintln!(" --record <file.raw> Record received audio to raw PCM file");
|
||||||
|
eprintln!(" (48kHz mono s16le, play with ffplay -f s16le -ar 48000 -ac 1 file.raw)");
|
||||||
|
eprintln!();
|
||||||
|
eprintln!("Default relay: 127.0.0.1:4433");
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
if relay_str.is_none() && !other.starts_with('-') {
|
||||||
|
relay_str = Some(other.to_string());
|
||||||
|
} else {
|
||||||
|
eprintln!("unknown argument: {other}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let relay_addr: SocketAddr = relay_str
|
||||||
|
.unwrap_or_else(|| "127.0.0.1:4433".to_string())
|
||||||
|
.parse()
|
||||||
|
.expect("invalid relay address");
|
||||||
|
|
||||||
|
CliArgs {
|
||||||
|
relay_addr,
|
||||||
|
live,
|
||||||
|
send_tone_secs,
|
||||||
|
record_file,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
tracing_subscriber::fmt().init();
|
tracing_subscriber::fmt().init();
|
||||||
|
|
||||||
let args: Vec<String> = std::env::args().collect();
|
let cli = parse_args();
|
||||||
let live = args.iter().any(|a| a == "--live");
|
|
||||||
let relay_addr: SocketAddr = args
|
|
||||||
.iter()
|
|
||||||
.skip(1)
|
|
||||||
.find(|a| *a != "--live")
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_else(|| "127.0.0.1:4433".to_string())
|
|
||||||
.parse()?;
|
|
||||||
|
|
||||||
info!(%relay_addr, live, "WarzonePhone client connecting");
|
info!(
|
||||||
|
relay = %cli.relay_addr,
|
||||||
|
live = cli.live,
|
||||||
|
send_tone = ?cli.send_tone_secs,
|
||||||
|
record = ?cli.record_file,
|
||||||
|
"WarzonePhone client"
|
||||||
|
);
|
||||||
|
|
||||||
let client_config = wzp_transport::client_config();
|
let client_config = wzp_transport::client_config();
|
||||||
// Use same address family as the relay address to avoid IPv4/IPv6 mismatch.
|
let bind_addr = if cli.relay_addr.is_ipv6() {
|
||||||
let bind_addr = if relay_addr.is_ipv6() {
|
|
||||||
"[::]:0".parse()?
|
"[::]:0".parse()?
|
||||||
} else {
|
} else {
|
||||||
"0.0.0.0:0".parse()?
|
"0.0.0.0:0".parse()?
|
||||||
};
|
};
|
||||||
let endpoint = wzp_transport::create_endpoint(bind_addr, None)?;
|
let endpoint = wzp_transport::create_endpoint(bind_addr, None)?;
|
||||||
let connection =
|
let connection =
|
||||||
wzp_transport::connect(&endpoint, relay_addr, "localhost", client_config).await?;
|
wzp_transport::connect(&endpoint, cli.relay_addr, "localhost", client_config).await?;
|
||||||
|
|
||||||
info!("Connected to relay");
|
info!("Connected to relay");
|
||||||
|
|
||||||
let transport = Arc::new(wzp_transport::QuinnTransport::new(connection));
|
let transport = Arc::new(wzp_transport::QuinnTransport::new(connection));
|
||||||
|
|
||||||
if live {
|
if cli.live {
|
||||||
run_live(transport).await
|
run_live(transport).await
|
||||||
|
} else if cli.send_tone_secs.is_some() || cli.record_file.is_some() {
|
||||||
|
run_file_mode(transport, cli.send_tone_secs, cli.record_file).await
|
||||||
} else {
|
} else {
|
||||||
run_silence(transport).await
|
run_silence(transport).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Original test mode: send silence frames.
|
/// Send silence frames (connectivity test).
|
||||||
async fn run_silence(transport: Arc<wzp_transport::QuinnTransport>) -> anyhow::Result<()> {
|
async fn run_silence(transport: Arc<wzp_transport::QuinnTransport>) -> anyhow::Result<()> {
|
||||||
let config = CallConfig::default();
|
let config = CallConfig::default();
|
||||||
let mut encoder = CallEncoder::new(&config);
|
let mut encoder = CallEncoder::new(&config);
|
||||||
|
|
||||||
let frame_duration = tokio::time::Duration::from_millis(20);
|
let frame_duration = tokio::time::Duration::from_millis(20);
|
||||||
let pcm = vec![0i16; FRAME_SAMPLES]; // 20ms @ 48kHz silence
|
let pcm = vec![0i16; FRAME_SAMPLES];
|
||||||
|
|
||||||
let mut total_source = 0u64;
|
let mut total_source = 0u64;
|
||||||
let mut total_repair = 0u64;
|
let mut total_repair = 0u64;
|
||||||
@@ -90,25 +180,159 @@ async fn run_silence(transport: Arc<wzp_transport::QuinnTransport>) -> anyhow::R
|
|||||||
tokio::time::sleep(frame_duration).await;
|
tokio::time::sleep(frame_duration).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
info!(
|
info!(total_source, total_repair, total_bytes, "done — closing");
|
||||||
total_source,
|
transport.close().await?;
|
||||||
total_repair,
|
Ok(())
|
||||||
total_bytes,
|
}
|
||||||
"done — closing"
|
|
||||||
);
|
/// File/tone mode: send a test tone and/or record received audio.
|
||||||
|
async fn run_file_mode(
|
||||||
|
transport: Arc<wzp_transport::QuinnTransport>,
|
||||||
|
send_tone_secs: Option<u32>,
|
||||||
|
record_file: Option<String>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let config = CallConfig::default();
|
||||||
|
|
||||||
|
// --- Send task: generate tone and send ---
|
||||||
|
let send_transport = transport.clone();
|
||||||
|
let send_handle = tokio::spawn(async move {
|
||||||
|
let secs = match send_tone_secs {
|
||||||
|
Some(s) => s,
|
||||||
|
None => {
|
||||||
|
// No sending, just wait
|
||||||
|
tokio::signal::ctrl_c().await.ok();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut encoder = CallEncoder::new(&config);
|
||||||
|
let total_frames = (secs as u64) * 50; // 50 frames/sec at 20ms
|
||||||
|
let frame_duration = tokio::time::Duration::from_millis(20);
|
||||||
|
|
||||||
|
let mut total_source = 0u64;
|
||||||
|
let mut total_repair = 0u64;
|
||||||
|
|
||||||
|
info!(seconds = secs, frames = total_frames, "sending 440Hz tone");
|
||||||
|
|
||||||
|
for frame_idx in 0..total_frames {
|
||||||
|
let pcm = generate_sine_frame(440.0, 48_000, frame_idx);
|
||||||
|
let packets = match encoder.encode_frame(&pcm) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => {
|
||||||
|
error!("encode error: {e}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for pkt in &packets {
|
||||||
|
if pkt.header.is_repair {
|
||||||
|
total_repair += 1;
|
||||||
|
} else {
|
||||||
|
total_source += 1;
|
||||||
|
}
|
||||||
|
if let Err(e) = send_transport.send_media(pkt).await {
|
||||||
|
error!("send error: {e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (frame_idx + 1) % 250 == 0 {
|
||||||
|
info!(
|
||||||
|
frame = frame_idx + 1,
|
||||||
|
source = total_source,
|
||||||
|
repair = total_repair,
|
||||||
|
"send progress"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
tokio::time::sleep(frame_duration).await;
|
||||||
|
}
|
||||||
|
info!(total_source, total_repair, "tone send complete");
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Recv task: decode and write to file ---
|
||||||
|
let recv_transport = transport.clone();
|
||||||
|
let recv_handle = tokio::spawn(async move {
|
||||||
|
let record_path = match record_file {
|
||||||
|
Some(p) => p,
|
||||||
|
None => {
|
||||||
|
// No recording, just wait
|
||||||
|
tokio::signal::ctrl_c().await.ok();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut decoder = CallDecoder::new(&CallConfig::default());
|
||||||
|
let mut pcm_buf = vec![0i16; FRAME_SAMPLES];
|
||||||
|
let mut all_pcm: Vec<i16> = Vec::new();
|
||||||
|
let mut frames_received = 0u64;
|
||||||
|
|
||||||
|
info!(file = %record_path, "recording received audio");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match recv_transport.recv_media().await {
|
||||||
|
Ok(Some(pkt)) => {
|
||||||
|
decoder.ingest(pkt);
|
||||||
|
while let Some(n) = decoder.decode_next(&mut pcm_buf) {
|
||||||
|
all_pcm.extend_from_slice(&pcm_buf[..n]);
|
||||||
|
frames_received += 1;
|
||||||
|
if frames_received % 250 == 0 {
|
||||||
|
info!(
|
||||||
|
frames = frames_received,
|
||||||
|
samples = all_pcm.len(),
|
||||||
|
"recv progress"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
info!("connection closed by remote");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("recv error: {e}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write raw PCM to file
|
||||||
|
if !all_pcm.is_empty() {
|
||||||
|
let bytes: Vec<u8> = all_pcm
|
||||||
|
.iter()
|
||||||
|
.flat_map(|s| s.to_le_bytes())
|
||||||
|
.collect();
|
||||||
|
if let Err(e) = std::fs::write(&record_path, &bytes) {
|
||||||
|
error!(file = %record_path, "write error: {e}");
|
||||||
|
} else {
|
||||||
|
let duration_secs = all_pcm.len() as f64 / 48_000.0;
|
||||||
|
info!(
|
||||||
|
file = %record_path,
|
||||||
|
frames = frames_received,
|
||||||
|
samples = all_pcm.len(),
|
||||||
|
duration_secs = format!("{:.1}", duration_secs),
|
||||||
|
bytes = bytes.len(),
|
||||||
|
"recording saved"
|
||||||
|
);
|
||||||
|
info!("play with: ffplay -f s16le -ar 48000 -ac 1 {record_path}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
info!("no audio received, nothing to write");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for both tasks
|
||||||
|
let _ = tokio::join!(send_handle, recv_handle);
|
||||||
|
|
||||||
transport.close().await?;
|
transport.close().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Live mode: capture from mic, encode, send; receive, decode, play.
|
/// Live mode: capture from mic, encode, send; receive, decode, play.
|
||||||
async fn run_live(transport: Arc<wzp_transport::QuinnTransport>) -> anyhow::Result<()> {
|
async fn run_live(transport: Arc<wzp_transport::QuinnTransport>) -> anyhow::Result<()> {
|
||||||
|
use wzp_client::audio_io::{AudioCapture, AudioPlayback};
|
||||||
|
|
||||||
let capture = AudioCapture::start()?;
|
let capture = AudioCapture::start()?;
|
||||||
let playback = AudioPlayback::start()?;
|
let playback = AudioPlayback::start()?;
|
||||||
info!("Audio I/O started — press Ctrl+C to stop");
|
info!("Audio I/O started — press Ctrl+C to stop");
|
||||||
|
|
||||||
// --- Send task: mic -> encode -> transport ---
|
|
||||||
// AudioCapture::read_frame() is blocking, so we run this on a dedicated
|
|
||||||
// OS thread. We use the tokio Handle to call the async send_media.
|
|
||||||
let send_transport = transport.clone();
|
let send_transport = transport.clone();
|
||||||
let rt_handle = tokio::runtime::Handle::current();
|
let rt_handle = tokio::runtime::Handle::current();
|
||||||
let send_handle = std::thread::Builder::new()
|
let send_handle = std::thread::Builder::new()
|
||||||
@@ -119,7 +343,7 @@ async fn run_live(transport: Arc<wzp_transport::QuinnTransport>) -> anyhow::Resu
|
|||||||
loop {
|
loop {
|
||||||
let frame = match capture.read_frame() {
|
let frame = match capture.read_frame() {
|
||||||
Some(f) => f,
|
Some(f) => f,
|
||||||
None => break, // channel closed / stopped
|
None => break,
|
||||||
};
|
};
|
||||||
let packets = match encoder.encode_frame(&frame) {
|
let packets = match encoder.encode_frame(&frame) {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
@@ -137,7 +361,6 @@ async fn run_live(transport: Arc<wzp_transport::QuinnTransport>) -> anyhow::Resu
|
|||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// --- Recv task: transport -> decode -> speaker ---
|
|
||||||
let recv_transport = transport.clone();
|
let recv_transport = transport.clone();
|
||||||
let recv_handle = tokio::spawn(async move {
|
let recv_handle = tokio::spawn(async move {
|
||||||
let config = CallConfig::default();
|
let config = CallConfig::default();
|
||||||
@@ -152,7 +375,6 @@ async fn run_live(transport: Arc<wzp_transport::QuinnTransport>) -> anyhow::Resu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
// No packet available right now, yield briefly.
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
|
tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -163,14 +385,10 @@ async fn run_live(transport: Arc<wzp_transport::QuinnTransport>) -> anyhow::Resu
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wait for Ctrl+C
|
tokio::signal::ctrl_c().await?;
|
||||||
tokio::signal::ctrl_c()
|
|
||||||
.await
|
|
||||||
.expect("failed to listen for Ctrl+C");
|
|
||||||
info!("Shutting down...");
|
info!("Shutting down...");
|
||||||
|
|
||||||
recv_handle.abort();
|
recv_handle.abort();
|
||||||
// The send thread will exit once capture is dropped / stopped.
|
|
||||||
drop(send_handle);
|
drop(send_handle);
|
||||||
transport.close().await?;
|
transport.close().await?;
|
||||||
info!("done");
|
info!("done");
|
||||||
|
|||||||
95
scripts/build-linux.sh
Executable file
95
scripts/build-linux.sh
Executable file
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Build WarzonePhone Linux x86_64 release binaries using a Hetzner Cloud VPS.
|
||||||
|
# Prerequisites: hcloud CLI authenticated, SSH key "wz" registered.
|
||||||
|
#
|
||||||
|
# Usage: ./scripts/build-linux.sh
|
||||||
|
#
|
||||||
|
# Outputs: target/linux-x86_64/wzp-relay, wzp-client, wzp-bench
|
||||||
|
|
||||||
|
SSH_KEY_NAME="wz"
|
||||||
|
SSH_KEY_PATH="/Users/manwe/CascadeProjects/wzp"
|
||||||
|
SERVER_NAME="wzp-builder-$(date +%s)"
|
||||||
|
SERVER_TYPE="cx23"
|
||||||
|
IMAGE="ubuntu-24.04"
|
||||||
|
REMOTE_USER="root"
|
||||||
|
OUTPUT_DIR="target/linux-x86_64"
|
||||||
|
|
||||||
|
echo "=== WarzonePhone Linux Build ==="
|
||||||
|
|
||||||
|
# Ensure server gets deleted on any exit (success or failure)
|
||||||
|
cleanup() {
|
||||||
|
if [ -n "${SERVER_NAME:-}" ]; then
|
||||||
|
echo " Cleaning up server $SERVER_NAME..."
|
||||||
|
hcloud server delete "$SERVER_NAME" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
rm -f /tmp/wzp-src.tar.gz
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# 1. Create the build server
|
||||||
|
echo "[1/7] Creating Hetzner server..."
|
||||||
|
hcloud server create \
|
||||||
|
--name "$SERVER_NAME" \
|
||||||
|
--type "$SERVER_TYPE" \
|
||||||
|
--image "$IMAGE" \
|
||||||
|
--ssh-key "$SSH_KEY_NAME" \
|
||||||
|
--location fsn1 \
|
||||||
|
--quiet
|
||||||
|
|
||||||
|
SERVER_IP=$(hcloud server ip "$SERVER_NAME")
|
||||||
|
echo " Server: $SERVER_NAME @ $SERVER_IP"
|
||||||
|
|
||||||
|
# SSH options: skip host key check, use our key
|
||||||
|
SSH="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 -i $SSH_KEY_PATH $REMOTE_USER@$SERVER_IP"
|
||||||
|
SCP="scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i $SSH_KEY_PATH"
|
||||||
|
|
||||||
|
# 2. Wait for SSH to come up
|
||||||
|
echo "[2/7] Waiting for SSH..."
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
if $SSH "echo ok" &>/dev/null; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
# 3. Install build dependencies
|
||||||
|
echo "[3/7] Installing build dependencies..."
|
||||||
|
$SSH "apt-get update -qq && apt-get install -y -qq build-essential cmake pkg-config libasound2-dev curl git > /dev/null 2>&1"
|
||||||
|
|
||||||
|
# 4. Install Rust
|
||||||
|
echo "[4/7] Installing Rust..."
|
||||||
|
$SSH "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable > /dev/null 2>&1"
|
||||||
|
|
||||||
|
# 5. Upload source code
|
||||||
|
echo "[5/7] Uploading source code..."
|
||||||
|
# Create a tarball excluding target/ and .git/
|
||||||
|
tar czf /tmp/wzp-src.tar.gz \
|
||||||
|
--exclude='target' \
|
||||||
|
--exclude='.git' \
|
||||||
|
--exclude='.claude' \
|
||||||
|
-C /Users/manwe/CascadeProjects/warzonePhone .
|
||||||
|
|
||||||
|
$SCP /tmp/wzp-src.tar.gz "$REMOTE_USER@$SERVER_IP:/root/wzp-src.tar.gz"
|
||||||
|
$SSH "mkdir -p /root/warzonePhone && tar xzf /root/wzp-src.tar.gz -C /root/warzonePhone"
|
||||||
|
|
||||||
|
# 6. Build release binaries
|
||||||
|
echo "[6/7] Building release binaries (this takes a few minutes)..."
|
||||||
|
$SSH "source ~/.cargo/env && cd /root/warzonePhone && cargo build --release --bin wzp-relay --bin wzp-client --bin wzp-bench 2>&1" | tail -5
|
||||||
|
|
||||||
|
# 7. Download binaries
|
||||||
|
echo "[7/7] Downloading binaries..."
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
$SCP "$REMOTE_USER@$SERVER_IP:/root/warzonePhone/target/release/wzp-relay" "$OUTPUT_DIR/wzp-relay"
|
||||||
|
$SCP "$REMOTE_USER@$SERVER_IP:/root/warzonePhone/target/release/wzp-client" "$OUTPUT_DIR/wzp-client"
|
||||||
|
$SCP "$REMOTE_USER@$SERVER_IP:/root/warzonePhone/target/release/wzp-bench" "$OUTPUT_DIR/wzp-bench"
|
||||||
|
|
||||||
|
# Show results (server is deleted by EXIT trap)
|
||||||
|
echo ""
|
||||||
|
echo "=== Build Complete ==="
|
||||||
|
ls -lh "$OUTPUT_DIR"/wzp-*
|
||||||
|
echo ""
|
||||||
|
echo "Deploy with:"
|
||||||
|
echo " scp $OUTPUT_DIR/wzp-relay $OUTPUT_DIR/wzp-bench user@relay-server:~/"
|
||||||
|
echo " scp $OUTPUT_DIR/wzp-client $OUTPUT_DIR/wzp-bench user@destination:~/"
|
||||||
11
scripts/cleanup-builder.sh
Executable file
11
scripts/cleanup-builder.sh
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
# Clean up any wzp-builder servers left running
|
||||||
|
echo "Looking for wzp-builder servers..."
|
||||||
|
hcloud server list -o noheader | grep wzp-builder | while read -r line; do
|
||||||
|
id=$(echo "$line" | awk '{print $1}')
|
||||||
|
name=$(echo "$line" | awk '{print $2}')
|
||||||
|
echo " Deleting $name (id=$id)..."
|
||||||
|
hcloud server delete "$id"
|
||||||
|
done
|
||||||
|
echo "Done."
|
||||||
Reference in New Issue
Block a user