mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
Update regorus core crate and all language bindings (ffi, java, python, wasm, ruby, csharp) to version 0.10.0. - Centralize C# package version via Directory.Packages.props - Remove redundant C# version suffix properties from project files - Regenerate all binding Cargo.lock files including Ruby - Fix xtask to read/write RegorusPackageVersion from Directory.Packages.props instead of parsing VersionPrefix from the csproj (which now uses an MSBuild property indirection) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
642 lines
22 KiB
Rust
642 lines
22 KiB
Rust
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
|
|
//! Keeps every language binding in lockstep with the core `regorus` crate version.
|
|
//!
|
|
//! The command follows the pattern popularised by large Rust workspaces such as
|
|
//! [`rust-analyzer`](https://github.com/rust-lang/rust-analyzer),
|
|
//! [`gitoxide`](https://github.com/Byron/gitoxide), and
|
|
//! [`ripgrep`](https://github.com/BurntSushi/ripgrep): a dedicated `xtask`
|
|
//! binary shells out to Git to detect source changes and rewrites the affected
|
|
//! manifests in one shot. That approach gives us deterministic edits (the Rust
|
|
//! code owns every write) while piggybacking on Git's knowledge of tracked vs
|
|
//! ignored files, so we avoid re-implementing ignore logic or path matching by
|
|
//! hand.
|
|
|
|
// Version management quick reference:
|
|
// - Run `cargo xtask bindings` after bumping the root crate; untouched bindings align to the core (e.g. root 0.5.2 pulls a quiet binding from 0.5.1 → 0.5.2).
|
|
// - Binding edits trigger a SemVer "minor" bump even while major = 0: 0.5.1+changes → 0.6.0, and 0.5.3+changes still lands on 0.6.0 even if the root sits at 0.5.2.
|
|
// - When the root races ahead, we follow it: a clean binding at 0.6.2 syncing against root 0.7.0 ends up at 0.7.0.
|
|
// - Use `cargo xtask bindings --check` in CI or pre-commit to fail fast when any manifest would be rewritten.
|
|
|
|
use std::collections::HashSet;
|
|
use std::ffi::OsStr;
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::Command;
|
|
|
|
use anyhow::{anyhow, bail, Context, Result};
|
|
use clap::Args;
|
|
use regex::Regex;
|
|
use semver::{BuildMetadata, Prerelease, Version};
|
|
use toml_edit::{value, DocumentMut};
|
|
|
|
/// CLI entry point for `cargo xtask bindings`.
|
|
#[derive(Args)]
|
|
pub struct BindingsCommand {
|
|
/// Override the version applied to bindings (defaults to regorus crate version)
|
|
#[arg(long, value_name = "VERSION")]
|
|
new_version: Option<String>,
|
|
|
|
/// Compare binding sources against this git ref (defaults to merge-base with origin/main)
|
|
#[arg(long, value_name = "REF")]
|
|
base_ref: Option<String>,
|
|
|
|
/// Return an error instead of writing files when updates are needed
|
|
#[arg(long)]
|
|
check: bool,
|
|
}
|
|
|
|
impl BindingsCommand {
|
|
/// Executes the bindings update workflow and reports any edits performed.
|
|
pub fn run(&self) -> Result<()> {
|
|
let workspace_root = workspace_root();
|
|
let root_version = match self.new_version.as_deref() {
|
|
Some(explicit) => Version::parse(explicit)
|
|
.with_context(|| format!("invalid --new-version '{explicit}'"))?,
|
|
None => read_root_crate_version(&workspace_root)?,
|
|
};
|
|
|
|
let watch_dirs = collect_watch_dirs();
|
|
let changed_paths =
|
|
collect_changed_paths(&workspace_root, self.base_ref.as_deref(), &watch_dirs)?;
|
|
|
|
let mut touched_paths = Vec::new();
|
|
let mut summaries = Vec::new();
|
|
|
|
for binding in BINDINGS {
|
|
let binding_changed = binding_has_changes(binding, &changed_paths);
|
|
let current_version = read_binding_version(&workspace_root, binding)?;
|
|
let desired_version = desired_version(¤t_version, &root_version, binding_changed);
|
|
let mut files =
|
|
apply_binding_version(&workspace_root, binding, &desired_version, self.check)?;
|
|
let files_modified = !files.is_empty();
|
|
touched_paths.append(&mut files);
|
|
let version_changed = desired_version != current_version;
|
|
|
|
summaries.push(BindingSummary {
|
|
name: binding.name,
|
|
desired_version,
|
|
version_changed,
|
|
binding_changed,
|
|
files_modified,
|
|
});
|
|
}
|
|
|
|
if !self.check {
|
|
if touched_paths.is_empty() {
|
|
println!("Bindings already up to date (target {}).", root_version);
|
|
} else {
|
|
println!("Updated binding versions:");
|
|
for summary in summaries.into_iter().filter(|s| s.files_modified) {
|
|
let reason = if summary.binding_changed && summary.version_changed {
|
|
"binding changes detected"
|
|
} else if summary.version_changed {
|
|
"aligned with core version"
|
|
} else {
|
|
"metadata normalized"
|
|
};
|
|
println!(
|
|
" {} -> {} ({reason})",
|
|
summary.name, summary.desired_version
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Captures what happened for a single binding during a run.
|
|
struct BindingSummary {
|
|
name: &'static str,
|
|
desired_version: Version,
|
|
version_changed: bool,
|
|
binding_changed: bool,
|
|
files_modified: bool,
|
|
}
|
|
|
|
/// Static description of each binding and the artefacts that should be updated.
|
|
struct Binding {
|
|
name: &'static str,
|
|
watch: &'static [&'static str],
|
|
manifest: Option<&'static str>,
|
|
ruby_version: Option<&'static str>,
|
|
pom_xml: Option<&'static str>,
|
|
csharp_version_file: Option<&'static str>,
|
|
csharp_dependents: &'static [&'static str],
|
|
}
|
|
|
|
/// Shared empty slice to keep the binding declarations uncluttered.
|
|
const EMPTY: &[&str] = &[];
|
|
|
|
/// Declarative inventory of the bindings we keep in sync.
|
|
const BINDINGS: &[Binding] = &[
|
|
Binding {
|
|
name: "ffi",
|
|
watch: &["bindings/ffi"],
|
|
manifest: Some("bindings/ffi/Cargo.toml"),
|
|
ruby_version: None,
|
|
pom_xml: None,
|
|
csharp_version_file: None,
|
|
csharp_dependents: EMPTY,
|
|
},
|
|
Binding {
|
|
name: "java",
|
|
watch: &["bindings/java"],
|
|
manifest: Some("bindings/java/Cargo.toml"),
|
|
ruby_version: None,
|
|
pom_xml: Some("bindings/java/pom.xml"),
|
|
csharp_version_file: None,
|
|
csharp_dependents: EMPTY,
|
|
},
|
|
Binding {
|
|
name: "python",
|
|
watch: &["bindings/python"],
|
|
manifest: Some("bindings/python/Cargo.toml"),
|
|
ruby_version: None,
|
|
pom_xml: None,
|
|
csharp_version_file: None,
|
|
csharp_dependents: EMPTY,
|
|
},
|
|
Binding {
|
|
name: "wasm",
|
|
watch: &["bindings/wasm"],
|
|
manifest: Some("bindings/wasm/Cargo.toml"),
|
|
ruby_version: None,
|
|
pom_xml: None,
|
|
csharp_version_file: None,
|
|
csharp_dependents: EMPTY,
|
|
},
|
|
Binding {
|
|
name: "ruby",
|
|
watch: &["bindings/ruby"],
|
|
manifest: Some("bindings/ruby/ext/regorusrb/Cargo.toml"),
|
|
ruby_version: Some("bindings/ruby/lib/regorus/version.rb"),
|
|
pom_xml: None,
|
|
csharp_version_file: None,
|
|
csharp_dependents: EMPTY,
|
|
},
|
|
Binding {
|
|
name: "csharp",
|
|
watch: &["bindings/csharp"],
|
|
manifest: None,
|
|
ruby_version: None,
|
|
pom_xml: None,
|
|
csharp_version_file: Some("bindings/csharp/Directory.Packages.props"),
|
|
csharp_dependents: EMPTY,
|
|
},
|
|
];
|
|
|
|
/// Returns the workspace root (one level above this crate).
|
|
fn workspace_root() -> PathBuf {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.parent()
|
|
.expect("xtask resides in workspace root")
|
|
.to_path_buf()
|
|
}
|
|
|
|
/// Collects the unique set of directories whose contents we monitor for changes.
|
|
/// Collects the unique set of directories whose contents we monitor for changes.
|
|
fn collect_watch_dirs() -> Vec<&'static str> {
|
|
let mut seen = HashSet::new();
|
|
BINDINGS
|
|
.iter()
|
|
.flat_map(|binding| binding.watch.iter().copied())
|
|
.filter(|dir| seen.insert(*dir))
|
|
.collect()
|
|
}
|
|
|
|
/// Returns every binding-relative path that changed compared to the base
|
|
/// revision as well as unstaged or untracked files in the working tree.
|
|
fn collect_changed_paths(
|
|
root: &Path,
|
|
base_ref: Option<&str>,
|
|
watch_dirs: &[&'static str],
|
|
) -> Result<HashSet<String>> {
|
|
let mut paths = HashSet::new();
|
|
|
|
if !watch_dirs.is_empty() {
|
|
if let Some(base) = resolve_base_ref(root, base_ref)? {
|
|
let mut args: Vec<&str> = vec!["diff", "--name-only", base.as_str()];
|
|
args.push("--");
|
|
args.extend(watch_dirs.iter().copied());
|
|
let diff = run_git(root, args)?;
|
|
for line in diff.lines() {
|
|
let trimmed = line.trim();
|
|
if !trimmed.is_empty() {
|
|
paths.insert(trimmed.to_string());
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut status_args: Vec<&str> = vec!["status", "--porcelain"]; // covers tracked + untracked
|
|
status_args.push("--");
|
|
status_args.extend(watch_dirs.iter().copied());
|
|
let status = run_git(root, status_args)?;
|
|
for line in status.lines() {
|
|
if line.len() < 4 {
|
|
continue;
|
|
}
|
|
let details = line[3..].trim();
|
|
if details.is_empty() {
|
|
continue;
|
|
}
|
|
let path = details.split(" -> ").last().unwrap().to_string();
|
|
paths.insert(path);
|
|
}
|
|
}
|
|
|
|
Ok(paths)
|
|
}
|
|
|
|
/// Picks the git commit to diff against: explicit flag, merge-base with
|
|
/// `origin/main`, then the immediate parent commit as a fallback.
|
|
fn resolve_base_ref(root: &Path, explicit: Option<&str>) -> Result<Option<String>> {
|
|
if let Some(reference) = explicit {
|
|
return Ok(Some(reference.to_string()));
|
|
}
|
|
|
|
if let Ok(output) = run_git(root, ["merge-base", "HEAD", "origin/main"]) {
|
|
if let Some(line) = output.lines().next() {
|
|
let trimmed = line.trim();
|
|
if !trimmed.is_empty() {
|
|
return Ok(Some(trimmed.to_string()));
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Ok(output) = run_git(root, ["rev-parse", "HEAD^"]) {
|
|
if let Some(line) = output.lines().next() {
|
|
let trimmed = line.trim();
|
|
if !trimmed.is_empty() {
|
|
return Ok(Some(trimmed.to_string()));
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(None)
|
|
}
|
|
|
|
/// Runs `git` with the supplied arguments and returns stdout as UTF-8 text.
|
|
fn run_git<I, S>(root: &Path, args: I) -> Result<String>
|
|
where
|
|
I: IntoIterator<Item = S>,
|
|
S: AsRef<OsStr>,
|
|
{
|
|
let output = Command::new("git")
|
|
.current_dir(root)
|
|
.args(args)
|
|
.output()
|
|
.context("failed to invoke git")?;
|
|
if !output.status.success() {
|
|
bail!(
|
|
"git command failed: {}",
|
|
String::from_utf8_lossy(&output.stderr).trim()
|
|
);
|
|
}
|
|
String::from_utf8(output.stdout).context("git output was not valid UTF-8")
|
|
}
|
|
|
|
/// Checks whether any watched path for this binding changed.
|
|
fn binding_has_changes(binding: &Binding, changed_paths: &HashSet<String>) -> bool {
|
|
changed_paths.iter().any(|path| {
|
|
let path_ref = Path::new(path);
|
|
binding
|
|
.watch
|
|
.iter()
|
|
.any(|prefix| path_ref.starts_with(Path::new(prefix)))
|
|
})
|
|
}
|
|
|
|
/// Reads the version declared in the root `Cargo.toml`.
|
|
fn read_root_crate_version(root: &Path) -> Result<Version> {
|
|
let manifest = fs::read_to_string(root.join("Cargo.toml"))
|
|
.context("failed to read workspace Cargo.toml")?;
|
|
let doc = manifest
|
|
.parse::<DocumentMut>()
|
|
.context("failed to parse workspace Cargo.toml")?;
|
|
let version = doc["package"]["version"]
|
|
.as_str()
|
|
.ok_or_else(|| anyhow!("workspace Cargo.toml missing package.version"))?;
|
|
Version::parse(version)
|
|
.with_context(|| format!("invalid workspace package.version '{version}'"))
|
|
}
|
|
|
|
/// Reads the authoritative version for a binding (Cargo manifest or C# project).
|
|
fn read_binding_version(root: &Path, binding: &Binding) -> Result<Version> {
|
|
if let Some(manifest) = binding.manifest {
|
|
return read_manifest_version(&root.join(manifest));
|
|
}
|
|
if let Some(project) = binding.csharp_version_file {
|
|
return read_csharp_version(root.join(project));
|
|
}
|
|
bail!("binding '{}' missing version source", binding.name)
|
|
}
|
|
|
|
fn read_manifest_version(path: &Path) -> Result<Version> {
|
|
let contents =
|
|
fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?;
|
|
let doc = contents
|
|
.parse::<DocumentMut>()
|
|
.with_context(|| format!("failed to parse {}", path.display()))?;
|
|
let version = doc["package"]["version"]
|
|
.as_str()
|
|
.ok_or_else(|| anyhow!("{} missing package.version", path.display()))?;
|
|
Version::parse(version)
|
|
.with_context(|| format!("invalid version '{}' in {}", version, path.display()))
|
|
}
|
|
|
|
fn read_csharp_version(path: PathBuf) -> Result<Version> {
|
|
let contents =
|
|
fs::read_to_string(&path).with_context(|| format!("failed to read {}", path.display()))?;
|
|
let re = Regex::new(r#"(?s)<RegorusPackageVersion>(?P<value>[^<]+)</RegorusPackageVersion>"#)?;
|
|
let caps = re
|
|
.captures(&contents)
|
|
.ok_or_else(|| anyhow!("{} missing <RegorusPackageVersion> entry", path.display()))?;
|
|
let version = caps.name("value").unwrap().as_str().trim();
|
|
Version::parse(version).with_context(|| {
|
|
format!(
|
|
"invalid RegorusPackageVersion '{}' in {}",
|
|
version,
|
|
path.display()
|
|
)
|
|
})
|
|
}
|
|
|
|
/// Calculates the version to write, bumping the minor release when the binding
|
|
/// changed while never regressing below the current version.
|
|
fn desired_version(current: &Version, target: &Version, binding_changed: bool) -> Version {
|
|
let mut candidate = target.clone();
|
|
|
|
if binding_changed {
|
|
candidate.minor += 1;
|
|
candidate.patch = 0;
|
|
candidate.pre = Prerelease::EMPTY;
|
|
candidate.build = BuildMetadata::EMPTY;
|
|
}
|
|
|
|
if candidate < *current {
|
|
candidate = current.clone();
|
|
}
|
|
|
|
candidate
|
|
}
|
|
|
|
/// Applies the chosen version across every artefact owned by the binding.
|
|
fn apply_binding_version(
|
|
root: &Path,
|
|
binding: &Binding,
|
|
version: &Version,
|
|
check: bool,
|
|
) -> Result<Vec<String>> {
|
|
let mut touched = Vec::new();
|
|
let version_str = version.to_string();
|
|
|
|
if let Some(manifest) = binding.manifest {
|
|
if update_manifest_version(root, manifest, &version_str, check)? {
|
|
touched.push(manifest.to_string());
|
|
// Update Cargo.lock if it exists in the same directory
|
|
if let Some(parent) = root.join(manifest).parent() {
|
|
let lock_path = parent.join("Cargo.lock");
|
|
if lock_path.exists() && !check {
|
|
update_cargo_lock(root, manifest)?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(version_file) = binding.ruby_version {
|
|
if update_ruby_version(root, version_file, &version_str, check)? {
|
|
touched.push(version_file.to_string());
|
|
}
|
|
}
|
|
|
|
if let Some(pom_path) = binding.pom_xml {
|
|
if update_java_pom(root, pom_path, &version_str, check)? {
|
|
touched.push(pom_path.to_string());
|
|
}
|
|
}
|
|
|
|
if let Some(project) = binding.csharp_version_file {
|
|
touched.extend(update_csharp_version_files(
|
|
root,
|
|
project,
|
|
binding.csharp_dependents,
|
|
&version_str,
|
|
check,
|
|
)?);
|
|
}
|
|
|
|
Ok(touched)
|
|
}
|
|
|
|
/// Updates a Cargo manifest with the supplied version string.
|
|
fn update_manifest_version(
|
|
root: &Path,
|
|
manifest: &str,
|
|
version: &str,
|
|
check: bool,
|
|
) -> Result<bool> {
|
|
let path = root.join(manifest);
|
|
edit_file(&path, check, |contents| {
|
|
let mut doc = contents
|
|
.parse::<DocumentMut>()
|
|
.with_context(|| format!("failed to parse {}", path.display()))?;
|
|
let current = doc["package"]["version"]
|
|
.as_str()
|
|
.ok_or_else(|| anyhow!("{} missing package.version", path.display()))?;
|
|
if current == version {
|
|
return Ok(None);
|
|
}
|
|
doc["package"]["version"] = value(version);
|
|
Ok(Some(doc.to_string()))
|
|
})
|
|
}
|
|
|
|
/// Rewrites the Ruby gem version constant.
|
|
fn update_ruby_version(
|
|
root: &Path,
|
|
version_file: &str,
|
|
version: &str,
|
|
check: bool,
|
|
) -> Result<bool> {
|
|
let path = root.join(version_file);
|
|
edit_file(&path, check, |contents| {
|
|
let re = Regex::new(r#"(?m)^(?P<prefix>\s*VERSION\s*=\s*")(?P<value>[^"]+)(?P<suffix>")"#)?;
|
|
if let Some(caps) = re.captures(contents) {
|
|
if caps.name("value").unwrap().as_str() == version {
|
|
return Ok(None);
|
|
}
|
|
let updated = re.replace(contents, |caps: ®ex::Captures| {
|
|
format!("{}{}{}", &caps["prefix"], version, &caps["suffix"])
|
|
});
|
|
Ok(Some(updated.into_owned()))
|
|
} else {
|
|
bail!("{} missing VERSION constant", path.display());
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Updates the Maven `pom.xml` entry for the Java binding.
|
|
fn update_java_pom(root: &Path, pom_path: &str, version: &str, check: bool) -> Result<bool> {
|
|
let path = root.join(pom_path);
|
|
edit_file(&path, check, |contents| {
|
|
let re = Regex::new(
|
|
r#"(?s)(<artifactId>regorus-java</artifactId>\s*<version>)(?P<value>[^<]+)(</version>)"#,
|
|
)?;
|
|
if let Some(caps) = re.captures(contents) {
|
|
if caps.name("value").unwrap().as_str() == version {
|
|
return Ok(None);
|
|
}
|
|
let updated = re.replace(contents, |caps: ®ex::Captures| {
|
|
format!("{}{}{}", &caps[1], version, &caps[3])
|
|
});
|
|
Ok(Some(updated.into_owned()))
|
|
} else {
|
|
bail!("{} missing regorus-java version", path.display());
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Updates the NuGet packaging project and any sample/test consumers.
|
|
fn update_csharp_version_files(
|
|
root: &Path,
|
|
version_file: &str,
|
|
dependent_projects: &[&str],
|
|
version: &str,
|
|
check: bool,
|
|
) -> Result<Vec<String>> {
|
|
let mut touched = Vec::new();
|
|
let version_prop = Regex::new(
|
|
r#"(?s)(?P<prefix><RegorusPackageVersion>)(?P<value>[^<]+)(?P<suffix></RegorusPackageVersion>)"#,
|
|
)?;
|
|
|
|
let package_path = root.join(version_file);
|
|
if !version_prop.is_match(
|
|
&fs::read_to_string(&package_path)
|
|
.with_context(|| format!("failed to read {}", package_path.display()))?,
|
|
) {
|
|
anyhow::bail!(
|
|
"{} missing <RegorusPackageVersion> entry",
|
|
package_path.display()
|
|
);
|
|
}
|
|
if edit_file(&package_path, check, |contents| {
|
|
let mut changed = false;
|
|
let new_contents = version_prop
|
|
.replace(contents, |caps: ®ex::Captures| {
|
|
let current = caps.name("value").unwrap().as_str().trim();
|
|
if current == version {
|
|
caps[0].to_string()
|
|
} else {
|
|
changed = true;
|
|
format!("{}{}{}", &caps["prefix"], version, &caps["suffix"])
|
|
}
|
|
})
|
|
.into_owned();
|
|
|
|
if changed && new_contents != *contents {
|
|
Ok(Some(new_contents))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
})? {
|
|
touched.push(version_file.to_string());
|
|
}
|
|
|
|
let pkg_ref = Regex::new(
|
|
r#"(?i)(?P<prefix><Package(?:Reference|Version)[^>]*Include="microsoft\.regorus"[^>]*Version=")(?P<value>\d+\.\d+\.\d+)(?P<suffix>[^\"]*")"#,
|
|
)?;
|
|
|
|
for rel_path in dependent_projects {
|
|
let path = root.join(rel_path);
|
|
if edit_file(&path, check, |contents| {
|
|
let mut changed = false;
|
|
let replaced = pkg_ref.replace_all(contents, |caps: ®ex::Captures| {
|
|
let current = caps.name("value").unwrap().as_str();
|
|
if current == version {
|
|
caps[0].to_string()
|
|
} else {
|
|
changed = true;
|
|
format!("{}{}{}", &caps["prefix"], version, &caps["suffix"])
|
|
}
|
|
});
|
|
let new_contents = replaced.into_owned();
|
|
if changed && new_contents != *contents {
|
|
Ok(Some(new_contents))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
})? {
|
|
touched.push((*rel_path).to_string());
|
|
}
|
|
}
|
|
|
|
touched.sort();
|
|
touched.dedup();
|
|
Ok(touched)
|
|
}
|
|
|
|
/// Reads a file, applies a transformation, and writes it back unless `--check`
|
|
/// is set.
|
|
fn edit_file<F>(path: &Path, check: bool, apply: F) -> Result<bool>
|
|
where
|
|
F: FnOnce(&str) -> Result<Option<String>>,
|
|
{
|
|
let original =
|
|
fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?;
|
|
|
|
if let Some(updated) = apply(&original)? {
|
|
if check {
|
|
bail!("{} requires updates", path.display());
|
|
}
|
|
fs::write(path, updated).with_context(|| format!("failed to write {}", path.display()))?;
|
|
return Ok(true);
|
|
}
|
|
|
|
Ok(false)
|
|
}
|
|
|
|
/// Updates Cargo.lock after a manifest version change by running cargo update.
|
|
/// Uses --precise to only update the version entry without touching dependencies.
|
|
fn update_cargo_lock(root: &Path, manifest: &str) -> Result<()> {
|
|
let manifest_path = root.join(manifest);
|
|
|
|
// Read the package name and version from the manifest
|
|
let contents = fs::read_to_string(&manifest_path)
|
|
.with_context(|| format!("failed to read {}", manifest_path.display()))?;
|
|
let doc = contents
|
|
.parse::<DocumentMut>()
|
|
.with_context(|| format!("failed to parse {}", manifest_path.display()))?;
|
|
|
|
let package_name = doc["package"]["name"]
|
|
.as_str()
|
|
.ok_or_else(|| anyhow!("{} missing package.name", manifest_path.display()))?;
|
|
let version = doc["package"]["version"]
|
|
.as_str()
|
|
.ok_or_else(|| anyhow!("{} missing package.version", manifest_path.display()))?;
|
|
|
|
let output = Command::new("cargo")
|
|
.arg("update")
|
|
.arg("--package")
|
|
.arg(package_name)
|
|
.arg("--precise")
|
|
.arg(version)
|
|
.arg("--manifest-path")
|
|
.arg(&manifest_path)
|
|
.output()
|
|
.context("failed to run cargo update")?;
|
|
|
|
if !output.status.success() {
|
|
bail!(
|
|
"cargo update failed for {}: {}",
|
|
manifest_path.display(),
|
|
String::from_utf8_lossy(&output.stderr).trim()
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|