require "fileutils" require "ostruct" module Qemu def self.qemu_bin_for(arch) { x86_64: "qemu-system-x86_64", x86_32: "qemu-system-i386", arm64: "qemu-system-aarch64", armv7: "qemu-system-arm", armv6: "qemu-system-arm", armv5: "qemu-system-arm", riscv64: "qemu-system-riscv64", ppc64le: "qemu-system-ppc64", ppc64: "qemu-system-ppc64", ppc32: "qemu-system-ppc", s390x: "qemu-system-s390x", mips64el: "qemu-system-mips64el", mips64: "qemu-system-mips64", mipsel: "qemu-system-mipsel", mips: "qemu-system-mips", loongarch64: "qemu-system-loongarch64", sparc64: "qemu-system-sparc64", alpha: "qemu-system-alpha", hppa: "qemu-system-hppa", ia64: "qemu-system-ia64", }.fetch(arch) { raise "Unsupported arch: #{arch.inspect}" } end def self.accel_args host = RbConfig::CONFIG["host_os"] if host =~ /linux/i && File.exist?("/dev/kvm") ["-accel", "kvm"] elsif host =~ /darwin/i # hvf exists on Apple Silicon + Intel macOS; QEMU falls back if not available ["-accel", "hvf"] elsif host =~ /freebsd/i # if QEMU was built with it; otherwise it will ignore ["-accel", "bhyve"] else ["-accel", "tcg"] end end def self.machine_args_for(arch) case arch when :x86_64, :x86_32 [] when :arm64 # -machine type=virt # -cpu cortex-a72 ["-machine", "virt", "-cpu", "max"] when :armv7, :armv6, :armv5 ["-machine", "virt", "-cpu", "cortex-a15"] when :riscv64 ["-machine", "virt"] when :loongarch64 ["-machine", "virt"] else [] end end #def self.launch(arch, disk_path, cdrom = nil, detach = true) def self.launch(arch, disk_path, **options) defaults = { arch: System::ARCH, cdrom: nil, detach: true, ram: 2048, cpus: 1, fullscreen: true } opts = defaults.merge(options) puts opts qemu = qemu_bin_for(arch) args = [] if System::OS == :macos && arch == :arm64 # args += ["-bios", "/opt/homebrew/share/qemu/edk2-aarch64-code.fd"] args += ["-drive", "if=pflash,format=raw,unit=0,readonly=on,file=/opt/homebrew/share/qemu/edk2-aarch64-code.fd"] #args += ["-device", "virtio-gpu-device"] if opts[:fullscreen] args += ["-display", "cocoa,full-screen=on"] else args += ["-display", "cocoa"] end args += ["-device", "qemu-xhci,id=xhci"] args += ["-device", "usb-kbd"] args += ["-device", "usb-tablet"] args += ["-device", "virtio-keyboard-device"] args += ["-device", "virtio-mouse-device"] args += ["-device", "virtio-gpu"] # copy to /Users/artur/.cache/dat/vm/debian/arm64/debian/vars.fd # /opt/homebrew/share/qemu/edk2-aarch64-vars.fd # args += ["-drive", "if=pflash,format=raw,unit=1,file=/Users/artur/.cache/dat/vm/debian/arm64/debian/vars.fd"] end args += accel_args args += machine_args_for(arch) args += ["-m", opts[:ram].to_s, "-smp", opts[:cpus].to_s] args += ["-name", "FirstVM", "-boot", "order=d"] # boot from CD first args += ["-drive", "file=#{disk_path},if=virtio,cache=writeback,format=raw"] if opts[:cdrom] != nil args += ["-cdrom", opts[:cdrom]] end args += ["-device", "virtio-net,netdev=n0", "-netdev", "user,id=n0"] # user-mode NAT # optional: uncomment to run headless with VNC on :5901 # args += ["-display", "none", "-vnc", "127.0.0.1:1"] cmd = [qemu, *args] puts "Launching: #{cmd.join(' ')}" pid = Process.spawn(*cmd, pgroup: true, out: $stdout, err: $stderr) if opts[:detach] Process.detach(pid) puts "QEMU pid=#{pid}" else Process.wait(pid) status = $? puts "Exit status: #{status.exitstatus}" end end end