265 lines · 8.2 KB · Rust
RawDownload
#![allow(clippy::disallowed_methods, reason = "tooling is exempt")] use std::fs;use std::path::{Path, PathBuf};use std::process::Command; use anyhow::{Context as _, Result, bail};use cargo_toml::Manifest;use clap::Parser;use regex::Regex;use toml_edit::{DocumentMut, Item, Table, value}; use crate::workspace::load_workspace; const GITIGNORE_ENTRY: &str = ".webrtc-sys/";const LOCAL_DIR_NAME: &str = ".webrtc-sys";const ENV_VAR: &str = "LK_CUSTOM_WEBRTC"; #[derive(Parser)]pub struct SetupWebrtcArgs {    /// Re-download even if the target directory already exists.    #[arg(long)]    force: bool,     /// Override the host triple component used for the release artifact    /// (e.g. `mac-arm64-release`). Defaults to the current host.    #[arg(long)]    triple: Option<String>,     /// Skip writing to `~/.cargo/config.toml`. Useful when you only want the    /// archive on disk and intend to set `LK_CUSTOM_WEBRTC` yourself.    #[arg(long)]    no_cargo_config: bool,} pub fn run_setup_webrtc(args: SetupWebrtcArgs) -> Result<()> {    let metadata = load_workspace()?;    let workspace_root = metadata.workspace_root.as_std_path().to_path_buf();     let rev = read_webrtc_sys_rev(&workspace_root)?;    eprintln!("Pinned livekit-rust-sdks rev: {rev}");     let tag = fetch_webrtc_tag(&rev)?;    eprintln!("WEBRTC_TAG for that rev: {tag}");     let triple = match args.triple {        Some(triple) => triple,        None => host_webrtc_triple()?,    };    eprintln!("Target triple: {triple}");     let local_root = workspace_root.join(LOCAL_DIR_NAME);    let tag_dir = local_root.join(&tag);    let extracted_dir = tag_dir.join(&triple);     if extracted_dir.exists() && !args.force {        eprintln!(            "Already present at {}, skipping download.",            extracted_dir.display()        );    } else {        if extracted_dir.exists() {            fs::remove_dir_all(&extracted_dir)                .with_context(|| format!("removing stale {}", extracted_dir.display()))?;        }        fs::create_dir_all(&tag_dir).with_context(|| format!("creating {}", tag_dir.display()))?;        download_and_extract(&tag, &triple, &tag_dir)?;    }     let absolute = extracted_dir        .canonicalize()        .with_context(|| format!("canonicalizing {}", extracted_dir.display()))?;     ensure_gitignore_entry(&workspace_root)?;     if args.no_cargo_config {        eprintln!(            "Skipping ~/.cargo/config.toml update. Set {ENV_VAR}={} yourself.",            absolute.display()        );    } else {        update_cargo_config(&absolute)?;    }     eprintln!();    eprintln!("Done. {ENV_VAR} -> {}", absolute.display());    Ok(())} fn read_webrtc_sys_rev(workspace_root: &Path) -> Result<String> {    let manifest_path = workspace_root.join("Cargo.toml");    let manifest = Manifest::from_path(&manifest_path)        .with_context(|| format!("parsing {}", manifest_path.display()))?;     let patch = manifest        .patch        .get("crates-io")        .context("workspace Cargo.toml has no [patch.crates-io] section")?;    let dep = patch        .get("webrtc-sys")        .context("[patch.crates-io] is missing webrtc-sys")?;    let detail = dep        .detail()        .context("webrtc-sys patch entry is not a table")?;    detail        .git        .as_ref()        .context("webrtc-sys patch is missing a git source")?;    detail        .rev        .clone()        .context("webrtc-sys patch is missing a `rev`")} fn fetch_webrtc_tag(rev: &str) -> Result<String> {    let url = format!(        "https://raw.githubusercontent.com/zed-industries/livekit-rust-sdks/{rev}/webrtc-sys/build/src/lib.rs"    );    let body = curl_text(&url).with_context(|| format!("fetching {url}"))?;     let re =        Regex::new(r#"pub\s+const\s+WEBRTC_TAG\s*:\s*&str\s*=\s*"([^"]+)""#).expect("static regex");    let captures = re        .captures(&body)        .with_context(|| format!("could not find WEBRTC_TAG in {url}"))?;    Ok(captures[1].to_string())} fn host_webrtc_triple() -> Result<String> {    let os = match std::env::consts::OS {        "macos" => "mac",        "linux" => "linux",        "windows" => "win",        other => bail!("unsupported host OS: {other}"),    };    let arch = match std::env::consts::ARCH {        "aarch64" => "arm64",        "x86_64" => "x64",        other => bail!("unsupported host arch: {other}"),    };    Ok(format!("{os}-{arch}-release"))} fn download_and_extract(tag: &str, triple: &str, into: &Path) -> Result<()> {    let url = format!(        "https://github.com/zed-industries/livekit-rust-sdks/releases/download/{tag}/webrtc-{triple}.zip"    );    let zip_path = into.join(format!("webrtc-{triple}.zip"));     eprintln!("Downloading {url}");    let status = Command::new("curl")        .args(["-fL", "--retry", "3", "--progress-bar", "-o"])        .arg(&zip_path)        .arg(&url)        .status()        .context("running curl")?;    if !status.success() {        bail!("curl exited with {status} while downloading {url}");    }     eprintln!("Extracting into {}", into.display());    let status = Command::new("unzip")        .arg("-q")        .arg("-o")        .arg(&zip_path)        .arg("-d")        .arg(into)        .status()        .context("running unzip")?;    if !status.success() {        bail!(            "unzip exited with {status} while extracting {}",            zip_path.display()        );    }     fs::remove_file(&zip_path).ok();    Ok(())} fn curl_text(url: &str) -> Result<String> {    let output = Command::new("curl")        .args(["-fsSL", url])        .output()        .context("running curl")?;    if !output.status.success() {        bail!(            "curl failed for {url} (exit {}): {}",            output.status,            String::from_utf8_lossy(&output.stderr).trim(),        );    }    String::from_utf8(output.stdout).context("curl returned non-UTF-8 body")} fn ensure_gitignore_entry(workspace_root: &Path) -> Result<()> {    let path = workspace_root.join(".gitignore");    let existing =        fs::read_to_string(&path).with_context(|| format!("reading {}", path.display()))?;    if existing        .lines()        .any(|line| line.trim() == GITIGNORE_ENTRY || line.trim() == LOCAL_DIR_NAME)    {        return Ok(());    }    let mut updated = existing;    if !updated.ends_with('\n') {        updated.push('\n');    }    updated.push_str(GITIGNORE_ENTRY);    updated.push('\n');    fs::write(&path, updated).with_context(|| format!("writing {}", path.display()))?;    eprintln!("Added {GITIGNORE_ENTRY} to .gitignore");    Ok(())} fn update_cargo_config(webrtc_path: &Path) -> Result<()> {    let home = std::env::var_os("HOME")        .or_else(|| std::env::var_os("USERPROFILE"))        .context("could not determine home directory")?;    let config_path = PathBuf::from(home).join(".cargo").join("config.toml");     if let Some(parent) = config_path.parent() {        fs::create_dir_all(parent).with_context(|| format!("creating {}", parent.display()))?;    }     let existing_content = if config_path.exists() {        fs::read_to_string(&config_path)            .with_context(|| format!("reading {}", config_path.display()))?    } else {        String::new()    };     let mut doc = existing_content        .parse::<DocumentMut>()        .with_context(|| format!("parsing existing {}", config_path.display()))?;     let env_table = doc        .entry("env")        .or_insert(Item::Table(Table::new()))        .as_table_mut()        .context("`env` entry is not a table")?;     let cleaned_path = clean_webrtc_path(webrtc_path)?;    env_table.insert(ENV_VAR, value(cleaned_path.clone()));     fs::write(&config_path, doc.to_string())        .with_context(|| format!("writing {}", config_path.display()))?;     eprintln!(        "Updated {} with {ENV_VAR}={cleaned_path}",        config_path.display()    );    Ok(())} fn clean_webrtc_path(path: &Path) -> Result<String> {    let path_str = path.to_str().context("webrtc path is not valid UTF-8")?;    let mut cleaned = path_str.to_string();    if cleaned.starts_with(r"\\?\") {        cleaned = cleaned[4..].to_string();    }    cleaned = cleaned.replace('\\', "/");    Ok(cleaned)}