codex-mobile-bridge 0.3.14

Remote bridge and service manager for codex-mobile.
Documentation
use super::*;

pub(super) fn activate_release(
    paths: &ManagedPaths,
    metadata: &ReleaseMetadata,
    env_values: &BridgeEnvValues,
    existing_record: Option<&InstallRecord>,
    operation: &str,
) -> Result<()> {
    environment::ensure_managed_directories(paths)?;
    let release_root = PathBuf::from(&metadata.release_root);
    environment::ensure_release_binary_link(&release_root)?;
    environment::write_managed_env(&paths.env_file, env_values)?;
    environment::write_user_service(&paths.unit_file)?;
    environment::point_current_release(&paths.current_link, &release_root)?;
    environment::daemon_reload()?;
    environment::ensure_service_started()?;
    let next_record = records::build_activate_record(existing_record, metadata, operation);
    environment::write_install_record(&paths.install_record_file, &next_record)?;
    Ok(())
}

pub(super) fn rollback_release(
    paths: &ManagedPaths,
    metadata: &ReleaseMetadata,
    env_values: &BridgeEnvValues,
    existing_record: &InstallRecord,
) -> Result<()> {
    environment::ensure_managed_directories(paths)?;
    let release_root = PathBuf::from(&metadata.release_root);
    environment::ensure_release_binary_link(&release_root)?;
    environment::write_managed_env(&paths.env_file, env_values)?;
    environment::write_user_service(&paths.unit_file)?;
    environment::point_current_release(&paths.current_link, &release_root)?;
    environment::daemon_reload()?;
    environment::ensure_service_started()?;
    let next_record = records::build_rollback_record(existing_record, metadata);
    environment::write_install_record(&paths.install_record_file, &next_record)?;
    Ok(())
}

pub(super) fn install_release(
    paths: &ManagedPaths,
    version: &str,
    cargo_binary: &str,
    registry: &str,
) -> Result<PathBuf> {
    let release_root = paths.release_root_for_version(version);
    fs::create_dir_all(&release_root)
        .with_context(|| format!("创建 release 目录失败: {}", release_root.display()))?;
    let cargo_home = prepare_isolated_cargo_home(paths)?;

    let output = Command::new(cargo_binary)
        .env("CARGO_HOME", &cargo_home)
        .env("CARGO_REGISTRIES_CRATES_IO_PROTOCOL", "sparse")
        .arg("install")
        .arg("--locked")
        .arg("--force")
        .arg("--registry")
        .arg(registry)
        .arg("--root")
        .arg(&release_root)
        .arg("--version")
        .arg(version)
        .arg("--bin")
        .arg(BINARY_NAME)
        .arg(CRATE_NAME)
        .output()
        .with_context(|| format!("执行 cargo install 失败: {cargo_binary}"))?;
    if !output.status.success() {
        bail!(
            "cargo install 失败(version={version}, registry={registry}): stdout={}; stderr={}",
            String::from_utf8_lossy(&output.stdout).trim(),
            String::from_utf8_lossy(&output.stderr).trim(),
        );
    }
    environment::ensure_release_binary_link(&release_root)?;
    Ok(release_root)
}

pub(super) fn resolve_latest_registry_version(
    paths: &ManagedPaths,
    cargo_binary: &str,
    registry: &str,
) -> Result<String> {
    let cargo_home = prepare_isolated_cargo_home(paths)?;
    let output = Command::new(cargo_binary)
        .env("CARGO_HOME", &cargo_home)
        .env("CARGO_REGISTRIES_CRATES_IO_PROTOCOL", "sparse")
        .arg("search")
        .arg(CRATE_NAME)
        .arg("--limit")
        .arg("1")
        .arg("--registry")
        .arg(registry)
        .output()
        .with_context(|| format!("执行 cargo search 失败: {cargo_binary}"))?;
    if !output.status.success() {
        bail!(
            "cargo search 失败(registry={registry}): stdout={}; stderr={}",
            String::from_utf8_lossy(&output.stdout).trim(),
            String::from_utf8_lossy(&output.stderr).trim(),
        );
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    let version = stdout
        .lines()
        .find_map(|line| parse_registry_search_line(line.trim()))
        .context("cargo search 未返回可解析的 bridge 版本")?;
    Ok(version)
}

pub(super) fn managed_paths() -> Result<ManagedPaths> {
    let home_dir = env::var_os("HOME")
        .map(PathBuf::from)
        .context("未找到 HOME 环境变量")?;
    Ok(ManagedPaths::new(home_dir))
}

pub(super) fn resolve_current_release_root() -> Result<PathBuf> {
    let current_exe = env::current_exe().context("读取当前可执行文件路径失败")?;
    let canonical = current_exe
        .canonicalize()
        .with_context(|| format!("解析当前可执行文件路径失败: {}", current_exe.display()))?;
    let parent = canonical.parent().context("当前可执行文件路径缺少父目录")?;
    if parent.file_name().and_then(|value| value.to_str()) == Some("bin") {
        return parent
            .parent()
            .map(Path::to_path_buf)
            .context("无法解析 release 根目录");
    }
    Ok(parent.to_path_buf())
}

pub(super) fn current_release_metadata(release_root: &Path) -> Result<ReleaseMetadata> {
    let executable_path =
        environment::release_binary_path(release_root).context("release 缺少 bridge 可执行文件")?;
    Ok(ReleaseMetadata {
        artifact_id: release_root
            .file_name()
            .and_then(|value| value.to_str())
            .unwrap_or(CRATE_NAME)
            .to_string(),
        version: BRIDGE_VERSION.to_string(),
        build_hash: BRIDGE_BUILD_HASH.to_string(),
        sha256: records::sha256_file(&executable_path)?,
        protocol_version: BRIDGE_PROTOCOL_VERSION as u32,
        release_root: release_root.to_string_lossy().to_string(),
        executable_path: executable_path.to_string_lossy().to_string(),
    })
}

pub(super) fn load_release_metadata(release_root: &Path) -> Result<ReleaseMetadata> {
    let current_exe = env::current_exe()
        .context("读取当前可执行文件路径失败")?
        .canonicalize()
        .context("解析当前可执行文件路径失败")?;
    let release_binary =
        environment::release_binary_path(release_root).context("release 缺少 bridge 可执行文件")?;
    let canonical_release_binary = release_binary
        .canonicalize()
        .with_context(|| format!("解析 release 可执行文件失败: {}", release_binary.display()))?;
    if canonical_release_binary == current_exe {
        return current_release_metadata(release_root);
    }

    let output = Command::new(&release_binary)
        .arg("manage")
        .arg("metadata")
        .output()
        .with_context(|| format!("执行 metadata 命令失败: {}", release_binary.display()))?;
    if !output.status.success() {
        bail!(
            "读取 release metadata 失败: stdout={}; stderr={}",
            String::from_utf8_lossy(&output.stdout).trim(),
            String::from_utf8_lossy(&output.stderr).trim(),
        );
    }
    let mut metadata: ReleaseMetadata =
        serde_json::from_slice(&output.stdout).context("解析 release metadata 失败")?;
    metadata.release_root = release_root.to_string_lossy().to_string();
    metadata.artifact_id = release_root
        .file_name()
        .and_then(|value| value.to_str())
        .unwrap_or(CRATE_NAME)
        .to_string();
    metadata.executable_path = release_binary.to_string_lossy().to_string();
    Ok(metadata)
}

pub(super) fn release_metadata_from_current_record(
    record: &InstallRecord,
    release_root: &Path,
) -> Option<ReleaseMetadata> {
    metadata_from_record(
        release_root,
        record.current_artifact_id.clone(),
        record.current_version.clone(),
        record.current_build_hash.clone(),
        record.current_sha256.clone(),
        record.current_protocol_version,
    )
}

pub(super) fn release_metadata_from_previous_record(
    record: &InstallRecord,
    release_root: &Path,
) -> Option<ReleaseMetadata> {
    metadata_from_record(
        release_root,
        record.previous_artifact_id.clone(),
        record.previous_version.clone(),
        record.previous_build_hash.clone(),
        record.previous_sha256.clone(),
        record.previous_protocol_version,
    )
}

fn metadata_from_record(
    release_root: &Path,
    artifact_id: Option<String>,
    version: Option<String>,
    build_hash: Option<String>,
    sha256: Option<String>,
    protocol_version: Option<u32>,
) -> Option<ReleaseMetadata> {
    let executable_path = environment::release_binary_path(release_root)?;
    Some(ReleaseMetadata {
        artifact_id: artifact_id.unwrap_or_else(|| {
            release_root
                .file_name()
                .and_then(|value| value.to_str())
                .unwrap_or(CRATE_NAME)
                .to_string()
        }),
        version: version?,
        build_hash: build_hash?,
        sha256: sha256?,
        protocol_version: protocol_version?,
        release_root: release_root.to_string_lossy().to_string(),
        executable_path: executable_path.to_string_lossy().to_string(),
    })
}

fn prepare_isolated_cargo_home(paths: &ManagedPaths) -> Result<PathBuf> {
    let cargo_home = paths.state_dir.join("cargo-home");
    fs::create_dir_all(&cargo_home)
        .with_context(|| format!("创建隔离 cargo home 失败: {}", cargo_home.display()))?;
    fs::write(
        cargo_home.join("config.toml"),
        isolated_cargo_config_contents(),
    )
    .with_context(|| format!("写入 cargo 配置失败: {}", cargo_home.display()))?;
    Ok(cargo_home)
}

pub(super) fn isolated_cargo_config_contents() -> &'static str {
    concat!(
        "[source.crates-io]\n",
        "registry = \"sparse+https://index.crates.io/\"\n\n",
        "[registries.crates-io]\n",
        "protocol = \"sparse\"\n\n",
        "[registry]\n",
        "global-credential-providers = [\"cargo:token\"]\n",
    )
}

fn parse_registry_search_line(line: &str) -> Option<String> {
    let quoted = line.split('"').nth(1)?;
    let version = quoted.trim();
    if version.is_empty() {
        return None;
    }
    Some(version.to_string())
}