use std::path::Path;
use std::process::Command;
use toml_edit::{DocumentMut, Item, Value};
use crate::init::SDK_VERSION;
use crate::update_check::parse_version;
use crate::{cargo, config, exit};
#[derive(Debug, PartialEq, Eq)]
pub enum Plan {
Current,
Upgrade(String),
Newer(String),
Local,
}
pub fn plan(cargo_toml: &str) -> anyhow::Result<(Plan, String)> {
let mut doc: DocumentMut = cargo_toml.parse()?;
let dep = doc
.get_mut("dependencies")
.and_then(|d| d.get_mut("oxframe"))
.filter(|d| !d.is_none())
.ok_or_else(|| anyhow::anyhow!("Cargo.toml has no `oxframe` dependency"))?;
let Some(req) = requirement(dep) else {
return Ok((Plan::Local, cargo_toml.to_owned()));
};
let current = req.trim_start_matches(['=', '^', '~', ' ']).to_owned();
let (Some(have), Some(want)) = (parse_version(¤t), parse_version(SDK_VERSION)) else {
anyhow::bail!("cannot read the oxframe version requirement `{req}`");
};
if have == want {
return Ok((Plan::Current, cargo_toml.to_owned()));
}
if have > want {
return Ok((Plan::Newer(current), cargo_toml.to_owned()));
}
let pinned = format!("={SDK_VERSION}");
match dep {
Item::Value(Value::String(s)) => {
let decor = s.decor().clone();
let mut v = toml_edit::Formatted::new(pinned);
*v.decor_mut() = decor;
*s = v;
}
Item::Value(Value::InlineTable(t)) => {
t.insert("version", pinned.into());
}
Item::Table(t) => {
t.insert("version", toml_edit::value(pinned));
}
_ => unreachable!("checked above"),
}
Ok((Plan::Upgrade(current), doc.to_string()))
}
fn requirement(dep: &Item) -> Option<String> {
let local = |t: &dyn toml_edit::TableLike| t.contains_key("path") || t.contains_key("git");
match dep {
Item::Value(Value::String(s)) => Some(s.value().clone()),
Item::Value(Value::InlineTable(t)) if !local(t) => {
t.get("version").and_then(|v| v.as_str()).map(str::to_owned)
}
Item::Table(t) if !local(t) => t.get("version").and_then(|v| v.as_str()).map(str::to_owned),
_ => None,
}
}
pub fn project_sdk_version(cargo_toml: &str) -> Option<String> {
let doc: DocumentMut = cargo_toml.parse().ok()?;
let req = requirement(doc.get("dependencies")?.get("oxframe")?)?;
let v = req.trim_start_matches(['=', '^', '~', ' ']);
parse_version(v).map(|_| v.to_owned())
}
pub fn run() -> u8 {
let root = match config::project_root() {
Ok(r) => r,
Err(e) => {
crate::error(e);
return exit::ENV;
}
};
let path = root.join("Cargo.toml");
let text = match std::fs::read_to_string(&path) {
Ok(t) => t,
Err(e) => {
crate::error(format!("{}: {e}", path.display()));
return exit::ENV;
}
};
let (plan, new_text) = match plan(&text) {
Ok(p) => p,
Err(e) => {
crate::error(format!("{}: {e}", path.display()));
return exit::ENV;
}
};
match &plan {
Plan::Current => crate::note(format!("already on oxframe {SDK_VERSION}")),
Plan::Local => crate::note("oxframe is a path/git dependency; leaving its version alone"),
Plan::Newer(v) => {
crate::error(format!(
"this project uses oxframe {v}, newer than this CLI supports ({SDK_VERSION}); \
update the CLI: cargo install oxframe-cli --force"
));
return exit::VERSION;
}
Plan::Upgrade(from) => {
if let Err(e) = std::fs::write(&path, new_text) {
crate::error(format!("{}: {e}", path.display()));
return exit::ENV;
}
crate::note(format!("oxframe {from} → {SDK_VERSION} in Cargo.toml"));
if !cargo_update(&root) {
return exit::BUILD;
}
}
}
match crate::agents::write(&root, false) {
Ok(r) => {
for f in &r.skipped {
crate::note(format!(
"kept {f} (edited by you; `oxframe agents --force` replaces it)"
));
}
}
Err(e) => crate::error(format!("cannot update agent files: {e}")),
}
if matches!(plan, Plan::Upgrade(_)) {
crate::note("done; run `oxframe check` to build with the new SDK");
}
exit::OK
}
fn cargo_update(root: &Path) -> bool {
match Command::new(cargo::cargo())
.current_dir(root)
.args(["update", "-p", "oxframe"])
.status()
{
Ok(s) => s.success(),
Err(e) => {
crate::error(format!("cannot run cargo: {e}"));
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn with_dep(dep: &str) -> String {
format!("[package]\nname = \"demo\"\n\n[dependencies]\n# the SDK\n{dep}\nserde = \"1\"\n")
}
#[test]
fn reads_the_projects_sdk_version() {
assert_eq!(
project_sdk_version(&with_dep("oxframe = \"=0.2.1\"")).as_deref(),
Some("0.2.1")
);
assert_eq!(
project_sdk_version(&with_dep("oxframe = { version = \"0.1\", features = [] }")),
None,
"not a full version"
);
assert_eq!(
project_sdk_version(&with_dep("oxframe = { path = \"../sdk\" }")),
None
);
assert_eq!(project_sdk_version("not toml ["), None);
}
#[test]
fn upgrades_inline_table_and_keeps_the_rest() {
let (p, out) = plan(&with_dep(
"oxframe = { version = \"=0.0.9\", default-features = false }",
))
.unwrap();
assert_eq!(p, Plan::Upgrade("0.0.9".into()));
assert!(
out.contains(&format!("version = \"={SDK_VERSION}\"")),
"{out}"
);
assert!(out.contains("default-features = false"));
assert!(out.contains("# the SDK\n"));
assert!(out.contains("serde = \"1\""));
}
#[test]
fn upgrades_plain_string() {
let (p, out) = plan(&with_dep("oxframe = \"0.0.1\"")).unwrap();
assert_eq!(p, Plan::Upgrade("0.0.1".into()));
assert!(
out.contains(&format!("oxframe = \"={SDK_VERSION}\"")),
"{out}"
);
}
#[test]
fn upgrades_dotted_table() {
let text = "[dependencies.oxframe]\nversion = \"0.0.1\"\nfeatures = [\"wasm\"]\n";
let (p, out) = plan(text).unwrap();
assert_eq!(p, Plan::Upgrade("0.0.1".into()));
assert!(
out.contains(&format!("version = \"={SDK_VERSION}\"")),
"{out}"
);
assert!(out.contains("features = [\"wasm\"]"));
}
#[test]
fn leaves_current_newer_and_local_alone() {
let current = with_dep(&format!("oxframe = \"={SDK_VERSION}\""));
assert_eq!(plan(¤t).unwrap().0, Plan::Current);
assert_eq!(
plan(&with_dep("oxframe = \"99.0.0\"")).unwrap().0,
Plan::Newer("99.0.0".into())
);
let local = with_dep("oxframe = { path = \"../oxframe-sdk/crates/oxframe\" }");
assert_eq!(plan(&local).unwrap(), (Plan::Local, local.clone()));
}
#[test]
fn needs_an_oxframe_dependency() {
assert!(plan("[dependencies]\nserde = \"1\"\n").is_err());
}
}