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"),
"[registries.crates-io]\nprotocol = \"sparse\"\n",
)
.with_context(|| format!("写入 cargo 配置失败: {}", cargo_home.display()))?;
Ok(cargo_home)
}
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())
}