Codex Handbook
core/src/config_lock.rs 179 lines
use std::io;use codex_config::ConfigLayerEntry;use codex_config::ConfigLayerSource;use codex_config::config_toml::ConfigLockfileToml;use codex_config::config_toml::ConfigToml;use codex_utils_absolute_path::AbsolutePathBuf;use serde::Serialize;use serde::de::DeserializeOwned;use similar::TextDiff;pub(crate) const CONFIG_LOCK_VERSION: u32 = 1;#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]pub(crate) struct ConfigLockReplayOptions {    pub allow_codex_version_mismatch: bool,}pub(crate) async fn read_config_lock_from_path(    path: &AbsolutePathBuf,) -> io::Result<ConfigLockfileToml> {    let contents = tokio::fs::read_to_string(path).await.map_err(|err| {        config_lock_error(format!(            "failed to read config lock file {}: {err}",            path.display()        ))    })?;    let lockfile: ConfigLockfileToml = toml::from_str(&contents).map_err(|err| {        config_lock_error(format!(            "failed to parse config lock file {}: {err}",            path.display()        ))    })?;    validate_config_lock_metadata_shape(&lockfile)?;    Ok(lockfile)}pub(crate) fn config_lockfile(config: ConfigToml) -> ConfigLockfileToml {    ConfigLockfileToml {        version: CONFIG_LOCK_VERSION,        codex_version: env!("CARGO_PKG_VERSION").to_string(),        config,    }}pub(crate) fn validate_config_lock_replay(    expected_lock: &ConfigLockfileToml,    actual_lock: &ConfigLockfileToml,    options: ConfigLockReplayOptions,) -> io::Result<()> {    validate_config_lock_metadata_shape(expected_lock)?;    validate_config_lock_metadata_shape(actual_lock)?;    if !options.allow_codex_version_mismatch        && expected_lock.codex_version != actual_lock.codex_version    {        return Err(config_lock_error(format!(            "config lock Codex version mismatch: lock was generated by {}, current version is {}; set debug.config_lockfile.allow_codex_version_mismatch=true to ignore this",            expected_lock.codex_version, actual_lock.codex_version        )));    }    let expected_lock = config_lock_for_comparison(expected_lock, options);    let actual_lock = config_lock_for_comparison(actual_lock, options);    if expected_lock != actual_lock {        let diff = compact_diff("config", &expected_lock, &actual_lock)            .unwrap_or_else(|err| format!("failed to build config lock diff: {err}"));        return Err(config_lock_error(format!(            "replayed effective config does not match config lock: {diff}"        )));    }    Ok(())}pub(crate) fn lock_layer_from_config(    lock_path: &AbsolutePathBuf,    lockfile: &ConfigLockfileToml,) -> io::Result<ConfigLayerEntry> {    let value = toml_value(        &config_without_lock_controls(&lockfile.config),        "config lock",    )?;    Ok(ConfigLayerEntry::new(        ConfigLayerSource::User {            file: lock_path.clone(),            profile: None,        },        value,    ))}pub(crate) fn config_without_lock_controls(config: &ConfigToml) -> ConfigToml {    let mut config = config.clone();    clear_config_lock_debug_controls(&mut config);    config}pub(crate) fn clear_config_lock_debug_controls(config: &mut ConfigToml) {    if let Some(debug) = config.debug.as_mut() {        debug.config_lockfile = None;    }    if config        .debug        .as_ref()        .is_some_and(|debug| debug.config_lockfile.is_none())    {        config.debug = None;    }}fn validate_config_lock_metadata_shape(lock: &ConfigLockfileToml) -> io::Result<()> {    if lock.version != CONFIG_LOCK_VERSION {        return Err(config_lock_error(format!(            "unsupported config lock version {}; expected {CONFIG_LOCK_VERSION}",            lock.version        )));    }    Ok(())}fn config_lock_for_comparison(    lockfile: &ConfigLockfileToml,    options: ConfigLockReplayOptions,) -> ConfigLockfileToml {    let mut lockfile = lockfile.clone();    clear_config_lock_debug_controls(&mut lockfile.config);    if let Some(features) = lockfile.config.features.as_mut() {        features.clear_removed_compatibility_entries();    }    if options.allow_codex_version_mismatch {        lockfile.codex_version.clear();    }    lockfile}fn config_lock_error(message: impl Into<String>) -> io::Error {    io::Error::other(message.into())}fn compact_diff<T: Serialize>(root: &str, expected: &T, actual: &T) -> io::Result<String> {    let expected = toml::to_string_pretty(expected).map_err(|err| {        config_lock_error(format!(            "failed to serialize expected {root} lock TOML: {err}"        ))    })?;    let actual = toml::to_string_pretty(actual).map_err(|err| {        config_lock_error(format!(            "failed to serialize actual {root} lock TOML: {err}"        ))    })?;    Ok(TextDiff::from_lines(&expected, &actual)        .unified_diff()        .context_radius(2)        .header("expected", "actual")        .to_string())}fn toml_value<T: Serialize>(value: &T, label: &str) -> io::Result<toml::Value> {    toml::Value::try_from(value)        .map_err(|err| config_lock_error(format!("failed to serialize {label}: {err}")))}pub(crate) fn toml_round_trip<T>(value: &impl Serialize, label: &'static str) -> io::Result<T>where    T: DeserializeOwned + Serialize,{    let value = toml_value(value, label)?;    let toml = value.clone().try_into().map_err(|err| {        config_lock_error(format!("failed to convert {label} to TOML shape: {err}"))    })?;    let represented_value = toml_value(&toml, label)?;    if represented_value != value {        return Err(config_lock_error(format!(            "resolved {label} cannot be fully represented as TOML"        )));    }    Ok(toml)}