@@ -42,6 +42,11 @@ pub struct DevRunArgs {
4242 #[ arg( long) ]
4343 pub memory : Option < String > ,
4444
45+ /// Override persistent disk size (e.g., "40G", "512M"). Existing disks can
46+ /// be grown but never shrunk.
47+ #[ arg( long) ]
48+ pub disk : Option < String > ,
49+
4550 /// Interactive mode (allocate PTY).
4651 #[ arg( short, long) ]
4752 pub interactive : bool ,
@@ -131,6 +136,10 @@ struct ResourceConfig {
131136 cpus : Option < u8 > ,
132137 #[ serde( default ) ]
133138 memory : Option < String > ,
139+ /// Persistent disk size (e.g., "20G", "512M"). Defaults to 20 GiB.
140+ /// Existing disks are grown to this size on boot but never shrunk.
141+ #[ serde( default ) ]
142+ disk : Option < String > ,
134143}
135144
136145// ── Handlers ───────────────────────────────────────────────────────
@@ -145,6 +154,11 @@ pub async fn cmd_run(args: DevRunArgs) -> anyhow::Result<()> {
145154 . as_deref ( )
146155 . or ( config. resources . memory . as_deref ( ) ) ,
147156 ) ?;
157+ let disk_size_bytes = parse_disk_config (
158+ args. disk
159+ . as_deref ( )
160+ . or ( config. resources . disk . as_deref ( ) ) ,
161+ ) ?;
148162
149163 let volume_mounts = build_volume_mounts ( & config, & project_dir) ?;
150164
@@ -162,7 +176,7 @@ pub async fn cmd_run(args: DevRunArgs) -> anyhow::Result<()> {
162176 }
163177 }
164178
165- let disk_image_path = ensure_project_disk ( & sandbox_id) ?;
179+ let disk_image_path = ensure_project_disk ( & sandbox_id, disk_size_bytes ) ?;
166180
167181 let state_db = default_state_db_path ( ) ;
168182 let mut client = connect_control_plane_for_state_db ( & state_db) . await ?;
@@ -649,24 +663,90 @@ fn build_volume_mounts(
649663
650664// ── Persistent disk ────────────────────────────────────────────────
651665
652- fn ensure_project_disk ( sandbox_id : & str ) -> anyhow:: Result < PathBuf > {
666+ /// Default persistent disk size when `resources.disk` is not set in vz.json.
667+ const DEFAULT_PROJECT_DISK_BYTES : u64 = 20 * 1024 * 1024 * 1024 ;
668+
669+ /// Parse the `resources.disk` field (or `--disk` override) into bytes.
670+ /// Returns the default when unset.
671+ fn parse_disk_config ( raw : Option < & str > ) -> anyhow:: Result < u64 > {
672+ match raw {
673+ None => Ok ( DEFAULT_PROJECT_DISK_BYTES ) ,
674+ Some ( s) => {
675+ let trimmed = s. trim ( ) ;
676+ if trimmed. is_empty ( ) {
677+ Ok ( DEFAULT_PROJECT_DISK_BYTES )
678+ } else {
679+ crate :: ipsw:: parse_disk_size ( trimmed)
680+ . context ( "invalid resources.disk (use e.g., '20G', '512M')" )
681+ }
682+ }
683+ }
684+ }
685+
686+ fn ensure_project_disk ( sandbox_id : & str , disk_size : u64 ) -> anyhow:: Result < PathBuf > {
653687 let run_dir = home_dir ( ) ?. join ( ".vz" ) . join ( "run" ) . join ( sandbox_id) ;
654688 std:: fs:: create_dir_all ( & run_dir)
655689 . with_context ( || format ! ( "failed to create {}" , run_dir. display( ) ) ) ?;
656690
657691 let disk_path = run_dir. join ( "disk.img" ) ;
658692 if !disk_path. exists ( ) {
659- let disk_size: u64 = 20 * 1024 * 1024 * 1024 ;
660693 let file = std:: fs:: File :: create ( & disk_path)
661694 . with_context ( || format ! ( "failed to create disk image {}" , disk_path. display( ) ) ) ?;
662695 file. set_len ( disk_size)
663696 . context ( "failed to set disk image size" ) ?;
664- eprintln ! ( "Created 20 GiB persistent disk at {}" , disk_path. display( ) ) ;
697+ eprintln ! (
698+ "Created {} persistent disk at {}" ,
699+ format_bytes( disk_size) ,
700+ disk_path. display( )
701+ ) ;
702+ } else {
703+ // Grow (but never shrink) existing disks to match the requested size.
704+ let current = std:: fs:: metadata ( & disk_path)
705+ . with_context ( || format ! ( "failed to stat disk image {}" , disk_path. display( ) ) ) ?
706+ . len ( ) ;
707+ if disk_size > current {
708+ let file = std:: fs:: OpenOptions :: new ( )
709+ . write ( true )
710+ . open ( & disk_path)
711+ . with_context ( || format ! ( "failed to open disk image {}" , disk_path. display( ) ) ) ?;
712+ file. set_len ( disk_size)
713+ . context ( "failed to grow disk image" ) ?;
714+ eprintln ! (
715+ "Grew persistent disk from {} to {} at {}" ,
716+ format_bytes( current) ,
717+ format_bytes( disk_size) ,
718+ disk_path. display( )
719+ ) ;
720+ } else if disk_size < current {
721+ eprintln ! (
722+ "Ignoring requested disk size {} — existing disk is {} (shrinking would lose data). \
723+ Use `vz run --fresh` to recreate.",
724+ format_bytes( disk_size) ,
725+ format_bytes( current) ,
726+ ) ;
727+ }
665728 }
666729
667730 Ok ( disk_path)
668731}
669732
733+ fn format_bytes ( bytes : u64 ) -> String {
734+ const TIB : u64 = 1024 * 1024 * 1024 * 1024 ;
735+ const GIB : u64 = 1024 * 1024 * 1024 ;
736+ const MIB : u64 = 1024 * 1024 ;
737+ if bytes >= TIB && bytes % TIB == 0 {
738+ format ! ( "{} TiB" , bytes / TIB )
739+ } else if bytes >= GIB {
740+ if bytes % GIB == 0 {
741+ format ! ( "{} GiB" , bytes / GIB )
742+ } else {
743+ format ! ( "{:.1} GiB" , bytes as f64 / GIB as f64 )
744+ }
745+ } else {
746+ format ! ( "{} MiB" , bytes / MIB )
747+ }
748+ }
749+
670750/// Filter out the harmless `getcwd() failed` warning from stderr.
671751///
672752/// The Linux kernel's `getcwd()` syscall cannot resolve the dentry path
0 commit comments