Skip to content

Commit 8ba4932

Browse files
feat: configurable persistent disk size in vz.json
Add resources.disk field (and --disk CLI override) so projects can opt out of the hardcoded 20 GiB cap. Existing disks grow on boot but are never shrunk; use `vz run --fresh` to recreate smaller. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ea1ccc0 commit 8ba4932

3 files changed

Lines changed: 86 additions & 6 deletions

File tree

crates/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/vz-cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "vz-cli"
3-
version = "0.3.8"
3+
version = "0.3.9"
44
description = "CLI for managing containers and macOS VM sandboxes"
55
edition.workspace = true
66
rust-version.workspace = true

crates/vz-cli/src/commands/dev.rs

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)