Skip to content

Conversation

@cgwalters
Copy link
Collaborator

The core idea here is to create an experience where the virtual machine is closely bound to a specific directory (a "project") - always always a git repository.

Closes: #31

Assisted-by: Claude Code (Sonnet 4.5)

@gemini-code-assist
Copy link

Summary of Changes

Hello @cgwalters, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant new feature: the bcvk project command, designed to provide a streamlined, Vagrant-like experience for managing virtual machines. It allows users to define and control VMs within the context of a project directory, simplifying configuration and lifecycle management. Key enhancements include automatic VM shutdown when the controlling process exits, flexible bind mounts for host-guest file sharing, and a rapid iteration development mode with automatic bootc updates. These changes aim to make working with bootc-based VMs more intuitive and efficient for developers.

Highlights

  • New bcvk project Command: Introduces a new top-level command bcvk project with subcommands (init, up, ssh, down, rm, ls) to provide a Vagrant-like workflow for managing VMs tied to a specific project directory.
  • Project-Scoped VM Management: VMs are now closely bound to a project directory, configured via a .bcvk/config.toml file, simplifying setup and management for development environments.
  • Automatic VM Lifecycle Binding: Adds a --lifecycle-bind-parent option (enabled by default for project VMs) that automatically shuts down the VM when the parent bcvk process exits, using pidfd or /proc polling for robust monitoring.
  • Enhanced Bind Mounts: Introduces --bind and --bind-ro options for libvirt run and project VMs, allowing host directories to be automatically mounted into the guest VM as read-write or read-only via systemd mount units injected through SMBIOS credentials.
  • Rapid Development Iteration with Auto-Updates: The bcvk project up --auto-update command configures the VM to automatically check for and apply bootc updates every 30 seconds, streamlining the development cycle for bootc-based systems.
  • Improved Disk Image Cache Hashing: The cache hash for disk images now incorporates all relevant InstallOptions fields, ensuring more accurate cache invalidation when any installation parameter changes.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new project subcommand to bcvk, enabling Vagrant-like VM management scoped to a project directory. It includes subcommands for initializing, starting, SSHing into, stopping, and removing project VMs. The changes also add support for bind mounts, automatic updates, and lifecycle management. The code review focuses on correctness, maintainability, and potential security issues.

pub fn compute_cache_hash(&self, install_options: &InstallOptions) -> String {
install_options.compute_hash(&self.digest)
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This function delegates to InstallOptions::compute_hash(). It would be useful to add a comment here explaining why this delegation is necessary and what benefits it provides.


/// Write metadata to a file using extended attributes via rustix
pub fn write_to_file(&self, file: &File) -> Result<()> {
pub fn write_to_file(&self, file: &File, install_options: &InstallOptions) -> Result<()> {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

It's good that you're passing install_options here. It would be useful to add a comment explaining why install_options is needed here, and how it affects the cache hash.

Comment on lines +230 to +346
if opts.lifecycle_bind_parent {
spawn_lifecycle_monitor(&vm_name, connect_uri)
.with_context(|| "Failed to spawn lifecycle monitor")?;
println!("Lifecycle monitor started for domain '{}'", vm_name);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Consider adding a check to ensure that the lifecycle monitor is only spawned for persistent VMs (i.e., not transient VMs). Spawning it for transient VMs might not be useful, as they are meant to be short-lived.

Comment on lines 674 to 680

// Validate that guest path is absolute
if !guest_path.starts_with('/') {
return Err(color_eyre::eyre::eyre!(
"Guest path '{}' must be an absolute path",
guest_path
));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

It would be useful to add a comment here explaining why the guest path must be an absolute path.

Comment on lines 855 to 1022
// Add any additional metadata from caller
for (key, value) in &opts.metadata {
domain_builder = domain_builder.with_metadata(key, value);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Consider adding a comment explaining what the purpose of adding metadata from the caller is.

Comment on lines 1044 to 1176
for extra_cred in &opts.extra_smbios_credentials {
qemu_args.push("-smbios".to_string());
qemu_args.push(format!("type=11,value={}", extra_cred));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Consider adding a comment explaining what the purpose of adding extra SMBIOS credentials from opts is.

Comment on lines 1050 to 1054
qemu_args.extend(vec![
"-netdev".to_string(),
format!("user,id=ssh0,hostfwd=tcp::{}-:22", ssh_port),
"-device".to_string(),
"virtio-net-pci,netdev=ssh0,addr=0x3".to_string(),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Consider adding a comment explaining what the purpose of adding networking args is.

Comment on lines 527 to 529
metadata
.write_to_file(&file)
.write_to_file(&file, install_options)
.with_context(|| "Failed to write metadata to disk file")?;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

It's good that you're passing install_options here. It would be useful to add a comment explaining why install_options is needed here, and how it affects the cache hash.

Comment on lines 54 to 78
pub fn smbios_creds_for_storage_opts() -> Result<Vec<String>> {
// Create systemd unit that conditionally appends to /etc/environment
let unit_content = r#"[Unit]
Description=Setup STORAGE_OPTS for bcvk
DefaultDependencies=no
Before=systemd-user-sessions.service
[Service]
Type=oneshot
ExecStart=/bin/sh -c 'grep -q STORAGE_OPTS /etc/environment || echo STORAGE_OPTS=additionalimagestore=/run/host-container-storage >> /etc/environment'
RemainAfterExit=yes
"#;
let encoded_unit = data_encoding::BASE64.encode(unit_content.as_bytes());
let unit_cred = format!(
"io.systemd.credential.binary:systemd.extra-unit.bcvk-storage-opts.service={encoded_unit}"
);

// Create dropin for sysinit.target to pull in our unit
let dropin_content = "[Unit]\nWants=bcvk-storage-opts.service\n";
let encoded_dropin = data_encoding::BASE64.encode(dropin_content.as_bytes());
let dropin_cred = format!(
"io.systemd.credential.binary:systemd.unit-dropin.sysinit.target~bcvk-storage={encoded_dropin}"
);

Ok(vec![unit_cred, dropin_cred])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This function creates a systemd unit and a dropin. It would be useful to add a comment explaining what the purpose of each of these is.

Comment on lines 87 to 91
concat!(
"f /etc/environment.d/90-bcvk-storage.conf 0644 root root - STORAGE_OPTS=additionalimagestore=/run/host-container-storage\n",
"d /etc/systemd/system.conf.d 0755 root root -\n",
"f /etc/systemd/system.conf.d/90-bcvk-storage.conf 0644 root root - [Manager]\\nDefaultEnvironment=STORAGE_OPTS=additionalimagestore=/run/host-container-storage\n"
).to_string()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This function generates tmpfiles.d lines for STORAGE_OPTS. It would be useful to add a comment explaining what the purpose of these lines is.

@cgwalters cgwalters force-pushed the bind-units branch 2 times, most recently from b7ceaf5 to d5d6c6f Compare October 24, 2025 20:37
@sysvion
Copy link

sysvion commented Oct 27, 2025

I have used this while developing a backup solution when migrating a database. Well i tried but i haven't written a single line of code. Because i was constantly debugging this project for this pr.

Some of the things i tried to debug are changed in this pr. But the one that cost a lot of time is also on main. The issue is when i tried to run project up when the environment variable RUST_LOG=debug is set. But then it hung on the waiting for systemd ( 2025-10-26T22:34:40.720714Z DEBUG Status update: Some(WaitingForSystemd)

I can't find why it is stuck there, but if i accept the ai suggestion to don't propagate the log level environment in run_ephemeral.rs. The issue stop to exist. If i check the log of podman container, the last entry is that it is waiting for qemu to quit. I do not know how to check what the vm does, so i stop there.

Full podman log

$> podman logs -f 83a49502960b
2025-10-26T01:05:39.971602Z DEBUG Running QEMU implementation inside container
2025-10-26T01:05:39.972527Z DEBUG Container image systemd version: Some(SystemdVersion(257))
2025-10-26T01:05:39.987040Z DEBUG Target image has cloud-init: false
2025-10-26T01:05:39.987667Z DEBUG Found kernel at: "/run/source-image/usr/lib/modules/6.16.10-200.fc42.x86_64/vmlinuz"
2025-10-26T01:05:39.993057Z DEBUG Checking for host mounts directory: /run/host-mounts exists = true
2025-10-26T01:05:39.993195Z DEBUG Checking for systemd units directory: /run/systemd-units exists = false
2025-10-26T01:05:39.993303Z DEBUG Setting up virtiofs mount for hoststorage (ro)
2025-10-26T01:05:40.028667Z DEBUG Generated mount unit: run-virtiofs\x2dmnt\x2dhoststorage.mount (enabled in local-fs.target)
2025-10-26T01:05:40.028830Z DEBUG Injecting systemd units from /run/systemd-units
2025-10-26T01:05:40.028865Z DEBUG No systemd units to inject at /run/systemd-units/system
2025-10-26T01:05:40.029148Z DEBUG Generating SSH keypair at "/var/lib/bcvk/ssh"
2025-10-26T01:05:40.450931Z DEBUG Generated SSH keypair successfully
2025-10-26T01:05:40.451394Z DEBUG Allocating swap: 21474836480
2025-10-26T01:05:40.471044Z DEBUG Processing BOOTC_DISK_FILES: /run/disk-files/output:output:qcow2
2025-10-26T01:05:40.471194Z DEBUG Adding virtio-blk device: file=/run/disk-files/output, serial=output, format=Qcow2
2025-10-26T01:05:40.471233Z DEBUG Enabled SSH port forwarding: host port 2222 -> guest port 22
2025-10-26T01:05:40.471282Z DEBUG Enabling systemd notification debugging
2025-10-26T01:05:40.471376Z DEBUG Starting QEMU with systemd debugging enabled
2025-10-26T01:05:40.480820Z DEBUG virtiofsd at /usr/libexec/virtiofsd supports --readonly: true
2025-10-26T01:05:40.482451Z DEBUG Spawned virtiofsd: binary=/usr/libexec/virtiofsd, socket=/run/inner-shared/virtiofs.sock, shared_dir=/run/source-image, debug=false
2025-10-26T01:05:40.485309Z DEBUG virtiofsd at /usr/libexec/virtiofsd supports --readonly: true
2025-10-26T01:05:40.486727Z DEBUG Spawned virtiofsd: binary=/usr/libexec/virtiofsd, socket=/run/inner-shared/virtiofs-hoststorage.sock, shared_dir=/run/host-mounts/hoststorage, debug=false
2025-10-26T01:05:40.486851Z DEBUG Waiting for socket at /run/inner-shared/virtiofs-hoststorage.sock
2025-10-26T01:05:40.588140Z DEBUG virtiofsd socket created: /run/inner-shared/virtiofs-hoststorage.sock
2025-10-26T01:05:40.588226Z DEBUG Waiting for socket at /run/inner-shared/virtiofs.sock
2025-10-26T01:05:40.588273Z DEBUG virtiofsd socket created: /run/inner-shared/virtiofs.sock
2025-10-26T01:05:40.588309Z DEBUG Successfully allocated VSOCK CID: 3
2025-10-26T01:05:40.588651Z DEBUG Listening on AF_VSOCK cid: 4294967295 port: 588930382
2025-10-26T01:05:40.589027Z DEBUG Adding AF_VSOCK device with guest CID: 3
2025-10-26T01:05:40.589214Z DEBUG AF_VSOCK listener thread started, waiting for systemd notifications
2025-10-26T01:05:40.589115Z DEBUG "qemu-system-x86_64" "-m" "4096M" "-smp" "8" "-enable-kvm" "-cpu" "host" "-audio" "none" "-object" "memory-backend-memfd,id=mem,share=on,size=4096M" "-numa" "node,memdev=mem" "-drive" "file=/var/tmp/.tmpVdYIZ3,format=raw,if=none,id=drive0" "-device" "virtio-blk-pci,drive=drive0,serial=swap" "-drive" "file=/run/disk-files/output,format=qcow2,if=none,id=drive1" "-device" "virtio-blk-pci,drive=drive1,serial=output" "-kernel" "/run/qemu/kernel" "-initrd" "/run/qemu/initramfs" "-chardev" "socket,id=char0,path=/run/inner-shared/virtiofs.sock" "-device" "vhost-user-fs-pci,queue-size=1024,chardev=char0,tag=rootfs" "-append" "rootfstype=virtiofs root=rootfs rootflags=ro selinux=0 systemd.volatile=overlay" "-chardev" "socket,id=char1,path=/run/inner-shared/virtiofs-hoststorage.sock" "-device" "vhost-user-fs-pci,queue-size=1024,chardev=char1,tag=mount_hoststorage" "-device" "virtio-serial" "-netdev" "user,id=net0,hostfwd=tcp::2222-:22" "-device" "virtio-net-pci,netdev=net0" "-serial" "none" "-nographic" "-display" "none" "-monitor" "none" "-device" "vhost-vsock-pci,guest-cid=3,vhostfd=42" "-smbios" "type=11,value=io.systemd.credential.binary:tmpfiles.extra=ZCAvcm9vdC8uc3NoIDA3NTAgLSAtIC0KZit+IC9yb290Ly5zc2gvYXV0aG9yaXplZF9rZXlzIDcwMCAtIC0gLSBjM05vTFhKellTQkJRVUZCUWpOT2VtRkRNWGxqTWtWQlFVRkJSRUZSUVVKQlFVRkRRVkZEZEd3eFVFNVVRakpuVVVWVlFsRTRVV1J0YTJObWFERmthM0JvWjJ0TU1tMHhkRzAxVGtSclRIUkpWWGh0YzB0eVN6UnVTVnBxUm01UWF6Qm1aMkZoTWpZM1NrbDBNRUpCVURFd1Vtb3JZa3ByZGxGM2JuWnVRazl3TnpSb1kxbEtTMjFVSzFWaFVGaHVjVFZtVW5oUk1HMTJhekUyYkVKQ2JHSmthWGNyYTNkVWJFUlRXVlp0Um5abFdVZHRabkpGYVU5b1VXdEJXalEyWjJSUWFubEVLMmhMTTJ0NVlUTnBWa2hYWWtSSVkyVjZOMnRPZW1wV1JuWjRiM2N5Y1hKalRXTmljWGxEZFRVME5tSnFlSGh2Wms1WGNFeFVTV0pwYXpOUFUwaG5VMmxVV1hSalFteFdZVWROVFVaMk9VTm5PV3hHYVhkSVYyWjNjVmRKWjNseE0yVk1ObE5EY3pKSFZuTnRUVlZFZDFGTWVUTjZSa3h6T1c1bVNsaEtRbUl3VEZKa2QxRXpNbnB6TVZsQ1dEa3hNV0Z2ZUdrMldrVm9SekZEVlRKalpqZEtVa1JXVERjdmVYWk9iMGRQU0ZKU1RtSnRiRXBCY0hsa0syOXlNVWQyYWpOWE4xVk9NV3BSYUdkbWF6bGtXV0pzWldOaldVWmpUMXB2VWxOSU1qY3JNVlYyTjNwR2FWUjJiSEpEVDJsalIwTXhkbEJoVUhwc2FtZFNZbXR0VkhCRFdrY3pUVzVETkZac1lYRlBaV1l5YWtWMFREZzNTamh2ZGtZNVltVlVLM0IzTWtWaGNXaHJaa05GZHpkTmJtaDNRWE5UTlhWTlNqQkdSVzF6YUdGemFXaElMM1l2YUdKdFZrMWxjMGxXU1c5TlNVeElaSFJDZG5WcGExSm5WVllySzFrNGVYUTNaM1pTTXpGek4zTm5ZVGxDV1RGR2FHMUlMMnhNTlV4bmVGaGxjbms1V1Roa1NuSnpNV3RyWXpVd01rMDRkeXRqYkZGcWVXZExTME5sTXpSQ056QmxSbWRKZDBKclQwUm9aV2xGVlZNcmFuTlBTMEZGVTBKSmFHaDJNalZqY1dkemVYQkRNblZZZWtSbWFEaGlTWGd5VkhkamIyUm9hMjF0V2pCUk0yVldWaTgxVUROdlNtbERaRXh2WldwNk1GVjRXRUZ3VW1GSVVraEthVVJEZFhkYVVVaEZaWGM5UFNCaVkzWnJMWE56YUFvPQ==" "-smbios" "type=11,value=io.systemd.credential:vmm.notify_socket=vsock-stream:2:588930382"
2025-10-26T01:05:40.596815Z DEBUG Waiting for qemu exit

The reason i wanted the debug flag is to get more context for the following error.

error: Installing to disk: Creating rootfs: No root filesystem specified
Error:
   0: Failed to find or create base disk
   1: Failed to install bootc to base disk: "/home/aron/.local/share/libvirt/images/bootc-base-26b3c962ff3bf9a9.vtIVln.tmp.qcow2"
   2: SSH installation command failed with exit code: Some(1)

Location:
   crates/kit/src/to_disk.rs:465

This was thrown when the config file only contains the name of the image. And if i add the filesystem used. The error goes away. There was a default right.... Well i think forcing the user to set this setting can be good. So adding the filesystem to project init is appreciated.

I haven't started looking at how --auto-update works. Which i was hoping to test out. But it is now 02:01 AM and i really need to sleep.

@cgwalters cgwalters marked this pull request as draft October 27, 2025 19:19
cgwalters added a commit to cgwalters/bcvk-fork that referenced this pull request Oct 28, 2025
This matches bootc's setup (without the journal integration for now
as we don't strictly need it).

But specifically this fixes setting `RUST_LOG` and having that
break the monitor-status JSON stream:
bootc-dev#86 (comment)

Signed-off-by: Colin Walters <[email protected]>
cgwalters added a commit to cgwalters/bcvk-fork that referenced this pull request Oct 28, 2025
This matches bootc's setup (without the journal integration for now
as we don't strictly need it).

But specifically this fixes setting `RUST_LOG` and having that
break the monitor-status JSON stream:
bootc-dev#86 (comment)

Signed-off-by: Colin Walters <[email protected]>
cgwalters added a commit to cgwalters/bcvk-fork that referenced this pull request Oct 28, 2025
This matches bootc's setup (without the journal integration for now
as we don't strictly need it).

But specifically this fixes setting `RUST_LOG` and having that
break the monitor-status JSON stream:
bootc-dev#86 (comment)

Signed-off-by: Colin Walters <[email protected]>
@cgwalters
Copy link
Collaborator Author

But the one that cost a lot of time is also on main. The issue is when i tried to run project up when the environment variable RUST_LOG=debug is set.

Ah yeah, I reproduced that. Nasty bug, I think that caused me confusion in the past. I ran out of time in debugging it. I put up #98 which mitigates it.

cgwalters added a commit that referenced this pull request Oct 28, 2025
This matches bootc's setup (without the journal integration for now
as we don't strictly need it).

But specifically this fixes setting `RUST_LOG` and having that
break the monitor-status JSON stream:
#86 (comment)

Signed-off-by: Colin Walters <[email protected]>
@cgwalters
Copy link
Collaborator Author

This one is passing CI now, but there's like a lot of code here and it needs some more sanity checking. Leaving as draft for now.

@sysvion
Copy link

sysvion commented Oct 29, 2025

I have looked some time of what should be changed. And in this comment contains a patch, and below that there is a bullet list, containing what i have found.

For the next time. Should i create a pr to your personal fork to suggest a code change? Or is a patch in the comments fine.

you can add the patch with git apply

From e68fe416f500f92debfb87e690ab8a010d21ef98 Mon Sep 17 00:00:00 2001
From: Aron <[email protected]>
Date: Wed, 29 Oct 2025 17:33:47 +0100
Subject: [PATCH] project | libvirt: Minor config changes

I have changed some config changes like setting a default filesystem to
make it more aligned to what the code says. And what my expectations
are.

libvirt:
   - add default filesystem var. (ext4 because it is allready used)
   - libvirt run: use default filesystem (Add by ai. Not manualy checked. Maybe i shouldn't use it.)

project up:
 - use default fs
 - use relative path

project down:
 - add --force flag that is passed down to libvirt functions
 - inlined functions

Assisted-by: Cursor's automatic modelswicher
---
 crates/kit/src/libvirt/mod.rs  |  3 +++
 crates/kit/src/libvirt/run.rs  |  2 +-
 crates/kit/src/project/down.rs | 44 +++++++++++++++-------------------
 crates/kit/src/project/up.rs   | 10 ++++----
 4 files changed, 29 insertions(+), 30 deletions(-)

diff --git a/crates/kit/src/libvirt/mod.rs b/crates/kit/src/libvirt/mod.rs
index 34156e3..eabdb2c 100644
--- a/crates/kit/src/libvirt/mod.rs
+++ b/crates/kit/src/libvirt/mod.rs
@@ -23,6 +23,9 @@ pub const LIBVIRT_DEFAULT_MEMORY: &str = "4G";
 /// Default disk size for libvirt base disks
 pub const LIBVIRT_DEFAULT_DISK_SIZE: &str = "20G";
 
+/// Default filesystem for libvirt VMs
+pub const LIBVIRT_DEFAULT_FILESYSTEM: &str = "ext4";
+
 pub mod base_disks;
 pub mod base_disks_cli;
 pub mod domain;
diff --git a/crates/kit/src/libvirt/run.rs b/crates/kit/src/libvirt/run.rs
index 4a7c550..51fbffe 100644
--- a/crates/kit/src/libvirt/run.rs
+++ b/crates/kit/src/libvirt/run.rs
@@ -1003,7 +1003,7 @@ fn create_libvirt_domain_from_disk(
             opts.install
                 .filesystem
                 .as_ref()
-                .unwrap_or(&"ext4".to_string()),
+                .unwrap_or(&crate::libvirt::LIBVIRT_DEFAULT_FILESYSTEM.to_string()),
         )
         .with_metadata("bootc:network", &opts.network)
         .with_metadata("bootc:ssh-generated", "true")
diff --git a/crates/kit/src/project/down.rs b/crates/kit/src/project/down.rs
index db10158..e799a00 100644
--- a/crates/kit/src/project/down.rs
+++ b/crates/kit/src/project/down.rs
@@ -19,6 +19,9 @@ pub struct ProjectDownOpts {
     /// Remove the VM after shutting it down
     #[clap(long)]
     pub remove: bool,
+
+    #[clap(long)]
+    pub force: bool,
 }
 
 /// Run the project down command
@@ -38,18 +41,31 @@ pub fn run(opts: ProjectDownOpts) -> Result<()> {
     };
 
     if !check_vm_exists(&vm_name, &libvirt_opts)? {
-        println!("Project VM '{}' does not exist", vm_name);
+        println!("Project is already down. vm_name: '{}'", vm_name);
         return Ok(());
     }
 
     // Stop the VM
     println!("Shutting down project VM '{}'...", vm_name);
-    stop_vm(&vm_name, &libvirt_opts)?;
+
+    let stop_opts = libvirt::stop::LibvirtStopOpts {
+        name: vm_name.to_string(),
+        force: opts.force,
+        timeout: 60,
+    };
+
+    let _ = libvirt::stop::run(&libvirt_opts, stop_opts);
 
     // Remove if requested
     if opts.remove {
         println!("Removing project VM '{}'...", vm_name);
-        remove_vm(&vm_name, &libvirt_opts)?;
+        let rm_opts = libvirt::rm::LibvirtRmOpts {
+            name: vm_name.to_string(),
+            force: opts.force,
+            stop: false,
+        };
+
+        libvirt::rm::run(&libvirt_opts, rm_opts)?
     }
 
     Ok(())
@@ -68,25 +84,3 @@ fn check_vm_exists(name: &str, libvirt_opts: &LibvirtOptions) -> Result<bool> {
 
     Ok(domains.iter().any(|d| d.name == name))
 }
-
-/// Stop a VM
-fn stop_vm(name: &str, libvirt_opts: &LibvirtOptions) -> Result<()> {
-    let stop_opts = libvirt::stop::LibvirtStopOpts {
-        name: name.to_string(),
-        force: false,
-        timeout: 60,
-    };
-
-    libvirt::stop::run(libvirt_opts, stop_opts)
-}
-
-/// Remove a VM
-fn remove_vm(name: &str, libvirt_opts: &LibvirtOptions) -> Result<()> {
-    let rm_opts = libvirt::rm::LibvirtRmOpts {
-        name: name.to_string(),
-        force: false,
-        stop: false,
-    };
-
-    libvirt::rm::run(libvirt_opts, rm_opts)
-}
diff --git a/crates/kit/src/project/up.rs b/crates/kit/src/project/up.rs
index bfa403e..f1393be 100644
--- a/crates/kit/src/project/up.rs
+++ b/crates/kit/src/project/up.rs
@@ -219,9 +219,11 @@ fn create_vm(
         extra_smbios_credentials: vec![],
     };
 
-    if let Some(ref fs) = vm.filesystem {
-        run_opts.install.filesystem = Some(fs.clone());
-    }
+    run_opts.install.filesystem = Some(
+        vm.filesystem
+            .clone()
+            .unwrap_or_else(|| crate::libvirt::LIBVIRT_DEFAULT_FILESYSTEM.to_string()),
+    );
 
     // Bind project directory to /run/src read-only with auto-mount
     // (will fall back to read-write if libvirt doesn't support readonly virtiofs)
@@ -233,7 +235,7 @@ fn create_vm(
 
     // Add configured mounts using bind mount options
     for mount in config.mounts.iter().flatten() {
-        let mount_spec = format!("{}:{}", mount.host, mount.guest);
+        let mount_spec = format!("{}/{}:{}", project_dir.as_str(), mount.host, mount.guest);
         let bind_mount = mount_spec
             .parse()
             .with_context(|| format!("Failed to parse mount spec: {}", mount_spec))?;
-- 
2.51.0
  • add version number on config file. So we can change things easely
  • move mount /run/src to /run/bcvk/project-src
  • make bootc-fetch-apply-updates a higher prority that the one in my container
# I manualy changed bcvk sources to add 00- before the string 
# /run/systemd/generator.early/bootc-fetch-apply-updates.timer.d/00-bcvk-auto-update.conf
[Timer]
OnBootSec=
OnCalendar=
OnUnitActiveSec=
OnUnitInactiveSec=
OnBootSec=30
OnUnitActiveSec=30

# /usr/lib/systemd/system/bootc-fetch-apply-updates.timer.d/morning-update.conf
[Timer]
# Remove all triggers by assigning an empty string to any one of them
# https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html#OnActiveSec=
OnActiveSec=todo

# Update at a specific time of day
OnCalendar=*-*-* xx:xx:xx Europe/Amsterdam   # time set. Keeping secret.

# Disable randomization
RandomizedDelaySec=0

systemd is taking the OnCalendar

Running bootc update when ssh-d in works. Should my project change. Or bcvk?

  • Mount currently only accept a directory. It would be nice if it allows mounting a single file
  • add port forwarding to config.
  • ssh sometimes says permission denied
  • check how the systemd.units_dir config var works.

@cgwalters
Copy link
Collaborator Author

add port forwarding to config.

Yeah that's an obvious one, as is the VM sizes. So recently I came across https://devfile.io/ ...we could try to instead parse a subset of that. It already supports both CPU/memory and port forwarding and the basic one of a container image name.

https://devfile.io/docs/2.3.0/adding-a-container-component

@sysvion
Copy link

sysvion commented Oct 29, 2025

We could try to instead parse a subset of that. It already supports both CPU/memory and port forwarding and the basic one of a container image name.

For my interpretation. Do you mean: Should we use the same configuration file as what devfile.io uses. So that

We can have the advantage of maintainers who have something in the registry who maybe also gives support to make a bootable container.

But then we have the disadvantage that we have another project that we need to keep track and change the configuration file to track what they are doing.

cgwalters and others added 3 commits October 30, 2025 18:15
The core idea here is to create an experience where
the virtual machine is closely bound to a specific
directory (a "project") - always always a git repository.

Closes: bootc-dev#31

Assisted-by: Claude Code (Sonnet 4.5)
Signed-off-by: Colin Walters <[email protected]>
The project command was unconditionally creating readonly bind mounts,
which caused failures on libvirt versions < 11.0 that don't support
readonly virtiofs. This was breaking the project_upgrade_workflow test
in CI (libvirt 10.0).

Assisted-by: Claude Code (Sonnet 4.5)
Signed-off-by: Colin Walters <[email protected]>
I have changed some config changes like setting a default filesystem to
make it more aligned to what the code says. And what my expectations
are.

libvirt:
   - add default filesystem var. (ext4 because it is allready used)
   - libvirt run: use default filesystem (Add by ai. Not manualy checked. Maybe i shouldn't use it.)

project up:
 - use default fs
 - use relative path

project down:
 - add --force flag that is passed down to libvirt functions
 - inlined functions

Assisted-by: Cursor's automatic modelswicher
Signed-off-by: Colin Walters <[email protected]>
@cgwalters
Copy link
Collaborator Author

cgwalters commented Oct 31, 2025

For the next time. Should i create a pr to your personal fork to suggest a code change? Or is a patch in the comments fine.

git diff in a comment is totally sane for this! I squashed your changes in.

(edit: I also rebased 🏄 )

@sysvion
Copy link

sysvion commented Oct 31, 2025

It maybe also interesting to parse the config of bootc-image-builder for other parameters like which filesystem to use. Also creating users maybe also interesting

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add bcvk up, inspired by Vagrant

2 participants