1010//! - **Publish** is an optional shell command (`publish`, e.g. `npx jsr publish`). When present
1111//! the package publishes through `otf-release publish` (which then tags + makes the GitHub
1212//! Release); when absent the package is build-only.
13- //! - **No dependency graph / lockfile / ranges.**
13+ //! - **No dependency graph / ranges.** A generic package that versions a root `Cargo.toml`
14+ //! refreshes `Cargo.lock` after version writes so Rust build-only releases stay consistent.
1415
1516use std:: path:: { Path , PathBuf } ;
1617use std:: process:: Command ;
@@ -21,6 +22,8 @@ use toml_edit::{value, DocumentMut, Item};
2122
2223use otf_release_core:: adapter:: { Adapter , Bump , DepKind , Pkg } ;
2324
25+ use crate :: command:: { CommandRunner , SystemRunner } ;
26+
2427/// One generic project, as configured in `release.toml`.
2528#[ derive( Debug , Clone ) ]
2629pub struct GenericPkg {
@@ -37,13 +40,27 @@ pub struct GenericPkg {
3740pub struct GenericAdapter {
3841 root : PathBuf ,
3942 packages : Vec < GenericPkg > ,
43+ runner : Box < dyn CommandRunner > ,
4044}
4145
4246impl GenericAdapter {
4347 pub fn new ( root : impl Into < PathBuf > , packages : Vec < GenericPkg > ) -> Self {
4448 Self {
4549 root : root. into ( ) ,
4650 packages,
51+ runner : Box :: new ( SystemRunner ) ,
52+ }
53+ }
54+
55+ pub fn with_runner (
56+ root : impl Into < PathBuf > ,
57+ packages : Vec < GenericPkg > ,
58+ runner : Box < dyn CommandRunner > ,
59+ ) -> Self {
60+ Self {
61+ root : root. into ( ) ,
62+ packages,
63+ runner,
4764 }
4865 }
4966
@@ -57,6 +74,15 @@ impl GenericAdapter {
5774 fn manifest_path ( & self , pkg : & GenericPkg ) -> PathBuf {
5875 self . root . join ( & pkg. manifest )
5976 }
77+
78+ fn updates_cargo_lockfile ( & self , root : & Path ) -> bool {
79+ root. join ( "Cargo.lock" ) . exists ( )
80+ && self . packages . iter ( ) . any ( |pkg| {
81+ pkg. manifest
82+ . file_name ( )
83+ . is_some_and ( |name| name == "Cargo.toml" )
84+ } )
85+ }
6086}
6187
6288/// Find the version value for `field` in manifest `text`. Matches `"field"…:…"value"` (JSON) and
@@ -276,7 +302,13 @@ impl Adapter for GenericAdapter {
276302 Ok ( ( ) )
277303 }
278304
279- fn update_lockfile ( & self , _root : & Path ) -> Result < ( ) > {
305+ fn update_lockfile ( & self , root : & Path ) -> Result < ( ) > {
306+ if self . updates_cargo_lockfile ( root) {
307+ let out = self . runner . run ( "cargo" , & [ "update" , "--workspace" ] , root) ?;
308+ if !out. success {
309+ bail ! ( "`cargo update --workspace` failed:\n {}" , out. stderr) ;
310+ }
311+ }
280312 Ok ( ( ) )
281313 }
282314
@@ -327,6 +359,40 @@ impl Adapter for GenericAdapter {
327359#[ cfg( test) ]
328360mod tests {
329361 use super :: * ;
362+ use crate :: command:: { CommandOutput , CommandRunner } ;
363+ use std:: sync:: { Arc , Mutex } ;
364+
365+ type Calls = Arc < Mutex < Vec < ( String , Vec < String > , PathBuf ) > > > ;
366+
367+ #[ derive( Clone ) ]
368+ struct FakeRunner {
369+ out : CommandOutput ,
370+ calls : Calls ,
371+ }
372+
373+ impl FakeRunner {
374+ fn new ( success : bool , stdout : & str , stderr : & str ) -> Self {
375+ Self {
376+ out : CommandOutput {
377+ success,
378+ stdout : stdout. into ( ) ,
379+ stderr : stderr. into ( ) ,
380+ } ,
381+ calls : Arc :: new ( Mutex :: new ( Vec :: new ( ) ) ) ,
382+ }
383+ }
384+ }
385+
386+ impl CommandRunner for FakeRunner {
387+ fn run ( & self , program : & str , args : & [ & str ] , cwd : & Path ) -> Result < CommandOutput > {
388+ self . calls . lock ( ) . unwrap ( ) . push ( (
389+ program. to_string ( ) ,
390+ args. iter ( ) . map ( |arg| arg. to_string ( ) ) . collect ( ) ,
391+ cwd. to_path_buf ( ) ,
392+ ) ) ;
393+ Ok ( self . out . clone ( ) )
394+ }
395+ }
330396
331397 fn pkg ( name : & str , manifest : & str , publish : Option < & str > ) -> GenericPkg {
332398 GenericPkg {
@@ -390,6 +456,49 @@ mod tests {
390456 assert_eq ! ( a. discover_packages( ) . unwrap( ) [ 0 ] . version, "0.4.0" ) ;
391457 }
392458
459+ #[ test]
460+ fn cargo_toml_manifest_refreshes_cargo_lockfile ( ) {
461+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
462+ std:: fs:: write ( tmp. path ( ) . join ( "Cargo.toml" ) , "version = \" 0.1.0\" \n " ) . unwrap ( ) ;
463+ std:: fs:: write ( tmp. path ( ) . join ( "Cargo.lock" ) , "" ) . unwrap ( ) ;
464+ let runner = FakeRunner :: new ( true , "" , "" ) ;
465+ let calls = runner. calls . clone ( ) ;
466+ let a = GenericAdapter :: with_runner (
467+ tmp. path ( ) ,
468+ vec ! [ pkg( "x" , "Cargo.toml" , None ) ] ,
469+ Box :: new ( runner) ,
470+ ) ;
471+
472+ a. update_lockfile ( tmp. path ( ) ) . unwrap ( ) ;
473+
474+ assert_eq ! (
475+ * calls. lock( ) . unwrap( ) ,
476+ vec![ (
477+ "cargo" . to_string( ) ,
478+ vec![ "update" . to_string( ) , "--workspace" . to_string( ) ] ,
479+ tmp. path( ) . to_path_buf( )
480+ ) ]
481+ ) ;
482+ }
483+
484+ #[ test]
485+ fn non_cargo_manifest_does_not_refresh_lockfile ( ) {
486+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
487+ std:: fs:: write ( tmp. path ( ) . join ( "deno.json" ) , "{\" version\" :\" 1.0.0\" }" ) . unwrap ( ) ;
488+ std:: fs:: write ( tmp. path ( ) . join ( "Cargo.lock" ) , "" ) . unwrap ( ) ;
489+ let runner = FakeRunner :: new ( true , "" , "" ) ;
490+ let calls = runner. calls . clone ( ) ;
491+ let a = GenericAdapter :: with_runner (
492+ tmp. path ( ) ,
493+ vec ! [ pkg( "x" , "deno.json" , None ) ] ,
494+ Box :: new ( runner) ,
495+ ) ;
496+
497+ a. update_lockfile ( tmp. path ( ) ) . unwrap ( ) ;
498+
499+ assert ! ( calls. lock( ) . unwrap( ) . is_empty( ) ) ;
500+ }
501+
393502 #[ test]
394503 fn reads_and_writes_nested_json_version_field ( ) {
395504 let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
0 commit comments