Codex Handbook
core-plugins/src/marketplace_upgrade.rs 297 lines
mod activation;mod git;use self::activation::activate_marketplace_root;use self::activation::installed_marketplace_metadata_matches;use self::activation::write_installed_marketplace_metadata;use self::git::clone_git_source;use self::git::git_remote_revision;use crate::marketplace::find_marketplace_manifest_path;use crate::marketplace::validate_marketplace_root;use codex_config::CONFIG_TOML_FILE;use codex_config::ConfigLayerStack;use codex_config::MarketplaceConfigUpdate;use codex_config::record_user_marketplace;use codex_config::types::MarketplaceConfig;use codex_config::types::MarketplaceSourceType;use codex_plugin::validate_plugin_segment;use codex_utils_absolute_path::AbsolutePathBuf;use std::collections::HashMap;use std::path::Path;use std::path::PathBuf;use std::time::Duration;use tracing::warn;const INSTALLED_MARKETPLACES_DIR: &str = ".tmp/marketplaces";const MARKETPLACE_UPGRADE_GIT_TIMEOUT: Duration = Duration::from_secs(30);#[derive(Debug, Clone, PartialEq, Eq)]pub struct ConfiguredMarketplaceUpgradeError {    pub marketplace_name: String,    pub message: String,}#[derive(Debug, Default, Clone, PartialEq, Eq)]pub struct ConfiguredMarketplaceUpgradeOutcome {    pub selected_marketplaces: Vec<String>,    pub upgraded_roots: Vec<AbsolutePathBuf>,    pub errors: Vec<ConfiguredMarketplaceUpgradeError>,}#[derive(Debug, Clone, PartialEq, Eq)]struct ConfiguredGitMarketplace {    name: String,    source: String,    ref_name: Option<String>,    sparse_paths: Vec<String>,    last_revision: Option<String>,}impl ConfiguredMarketplaceUpgradeOutcome {    pub fn all_succeeded(&self) -> bool {        self.errors.is_empty()    }}pub fn configured_git_marketplace_names(config_layer_stack: &ConfigLayerStack) -> Vec<String> {    let mut names = configured_git_marketplaces(config_layer_stack)        .into_iter()        .map(|marketplace| marketplace.name)        .collect::<Vec<_>>();    names.sort_unstable();    names}pub fn upgrade_configured_git_marketplaces(    codex_home: &Path,    config_layer_stack: &ConfigLayerStack,    marketplace_name: Option<&str>,) -> ConfiguredMarketplaceUpgradeOutcome {    let marketplaces = configured_git_marketplaces(config_layer_stack)        .into_iter()        .filter(|marketplace| marketplace_name.is_none_or(|name| marketplace.name.as_str() == name))        .collect::<Vec<_>>();    if marketplaces.is_empty() {        return ConfiguredMarketplaceUpgradeOutcome::default();    }    let install_root = marketplace_install_root(codex_home);    let selected_marketplaces = marketplaces        .iter()        .map(|marketplace| marketplace.name.clone())        .collect();    let mut upgraded_roots = Vec::new();    let mut errors = Vec::new();    for marketplace in marketplaces {        match upgrade_configured_git_marketplace(codex_home, &install_root, &marketplace) {            Ok(Some(upgraded_root)) => upgraded_roots.push(upgraded_root),            Ok(None) => {}            Err(err) => {                errors.push(ConfiguredMarketplaceUpgradeError {                    marketplace_name: marketplace.name,                    message: err,                });            }        }    }    ConfiguredMarketplaceUpgradeOutcome {        selected_marketplaces,        upgraded_roots,        errors,    }}fn marketplace_install_root(codex_home: &Path) -> PathBuf {    codex_home.join(INSTALLED_MARKETPLACES_DIR)}fn configured_git_marketplaces(    config_layer_stack: &ConfigLayerStack,) -> Vec<ConfiguredGitMarketplace> {    let Some(user_config) = config_layer_stack.effective_user_config() else {        return Vec::new();    };    let Some(marketplaces_value) = user_config.get("marketplaces") else {        return Vec::new();    };    let marketplaces = match marketplaces_value        .clone()        .try_into::<HashMap<String, MarketplaceConfig>>()    {        Ok(marketplaces) => marketplaces,        Err(err) => {            warn!("invalid marketplaces config while preparing auto-upgrade: {err}");            return Vec::new();        }    };    let mut configured = marketplaces        .into_iter()        .filter_map(|(name, marketplace)| configured_git_marketplace_from_config(name, marketplace))        .collect::<Vec<_>>();    configured.sort_unstable_by(|left, right| left.name.cmp(&right.name));    configured}fn configured_git_marketplace_from_config(    name: String,    marketplace: MarketplaceConfig,) -> Option<ConfiguredGitMarketplace> {    let MarketplaceConfig {        last_updated: _,        last_revision,        source_type,        source,        ref_name,        sparse_paths,    } = marketplace;    if source_type != Some(MarketplaceSourceType::Git) {        return None;    }    let Some(source) = source else {        warn!(            marketplace = name,            "ignoring configured Git marketplace without source"        );        return None;    };    Some(ConfiguredGitMarketplace {        name,        source,        ref_name,        sparse_paths: sparse_paths.unwrap_or_default(),        last_revision,    })}fn upgrade_configured_git_marketplace(    codex_home: &Path,    install_root: &Path,    marketplace: &ConfiguredGitMarketplace,) -> Result<Option<AbsolutePathBuf>, String> {    validate_plugin_segment(&marketplace.name, "marketplace name")?;    let remote_revision = git_remote_revision(        &marketplace.source,        marketplace.ref_name.as_deref(),        MARKETPLACE_UPGRADE_GIT_TIMEOUT,    )?;    let destination = install_root.join(&marketplace.name);    if find_marketplace_manifest_path(&destination).is_some()        && marketplace.last_revision.as_deref() == Some(remote_revision.as_str())        && installed_marketplace_metadata_matches(&destination, marketplace, &remote_revision)    {        return Ok(None);    }    let staging_parent = install_root.join(".staging");    std::fs::create_dir_all(&staging_parent).map_err(|err| {        format!(            "failed to create marketplace upgrade staging directory {}: {err}",            staging_parent.display()        )    })?;    let staged_dir = tempfile::Builder::new()        .prefix("marketplace-upgrade-")        .tempdir_in(&staging_parent)        .map_err(|err| {            format!(                "failed to create temporary marketplace upgrade directory in {}: {err}",                staging_parent.display()            )        })?;    let activated_revision = clone_git_source(        &marketplace.source,        marketplace.ref_name.as_deref(),        &marketplace.sparse_paths,        staged_dir.path(),        MARKETPLACE_UPGRADE_GIT_TIMEOUT,    )?;    let marketplace_name = validate_marketplace_root(staged_dir.path())        .map_err(|err| format!("failed to validate upgraded marketplace root: {err}"))?;    if marketplace_name != marketplace.name {        return Err(format!(            "upgraded marketplace name `{marketplace_name}` does not match configured marketplace `{}`",            marketplace.name        ));    }    write_installed_marketplace_metadata(staged_dir.path(), marketplace, &activated_revision)?;    let last_updated = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true);    let update = MarketplaceConfigUpdate {        last_updated: &last_updated,        last_revision: Some(&activated_revision),        source_type: "git",        source: &marketplace.source,        ref_name: marketplace.ref_name.as_deref(),        sparse_paths: &marketplace.sparse_paths,    };    activate_marketplace_root(&destination, staged_dir, || {        ensure_configured_git_marketplace_unchanged(codex_home, marketplace)?;        record_user_marketplace(codex_home, &marketplace.name, &update).map_err(|err| {            format!(                "failed to record upgraded marketplace `{}` in user config.toml: {err}",                marketplace.name            )        })    })?;    AbsolutePathBuf::try_from(destination)        .map(Some)        .map_err(|err| format!("upgraded marketplace path is not absolute: {err}"))}fn ensure_configured_git_marketplace_unchanged(    codex_home: &Path,    expected: &ConfiguredGitMarketplace,) -> Result<(), String> {    let current = read_configured_git_marketplace(codex_home, &expected.name)?;    match current {        Some(current) if current == *expected => Ok(()),        Some(_) => Err(format!(            "configured marketplace `{}` changed while auto-upgrade was in flight",            expected.name        )),        None => Err(format!(            "configured marketplace `{}` was removed or is no longer a Git marketplace",            expected.name        )),    }}fn read_configured_git_marketplace(    codex_home: &Path,    marketplace_name: &str,) -> Result<Option<ConfiguredGitMarketplace>, String> {    let config_path = codex_home.join(CONFIG_TOML_FILE);    let raw_config = match std::fs::read_to_string(&config_path) {        Ok(raw_config) => raw_config,        Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),        Err(err) => {            return Err(format!(                "failed to read user config {} while checking marketplace auto-upgrade: {err}",                config_path.display()            ));        }    };    let config: toml::Value = toml::from_str(&raw_config).map_err(|err| {        format!(            "failed to parse user config {} while checking marketplace auto-upgrade: {err}",            config_path.display()        )    })?;    let Some(marketplaces_value) = config.get("marketplaces") else {        return Ok(None);    };    let mut marketplaces = marketplaces_value        .clone()        .try_into::<HashMap<String, MarketplaceConfig>>()        .map_err(|err| format!("invalid marketplaces config while checking auto-upgrade: {err}"))?;    let Some(marketplace) = marketplaces.remove(marketplace_name) else {        return Ok(None);    };    Ok(configured_git_marketplace_from_config(        marketplace_name.to_string(),        marketplace,    ))}