require "fileutils" require "ostruct" module DisplayMode def self.none 0 end def self.fullscreen 1 end def self.window 2 end end 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 * 8, cpus: 1, display: DisplayMode::none } # for testing only defaults[:detach] = false defaults[:display] = DisplayMode.fullscreen defaults[:display] = DisplayMode.window defaults[:display] = DisplayMode.none opts = defaults.merge(options) puts 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"] # cp /opt/homebrew/share/qemu/edk2-arm-vars.fd ~/edk2-arm-vars.fd args += ["-drive", "if=pflash,format=raw,unit=0,readonly=on,file=/opt/homebrew/share/qemu/edk2-aarch64-code.fd"] args += ["-drive", "if=pflash,format=raw,unit=1,file=/Users/agurgul/edk2-arm-vars.fd"] if opts[:display] == DisplayMode::none port = 2222 args += ['-nographic'] args += ['-netdev', "user,id=net0,hostfwd=tcp:127.0.0.1:#{port}-:22"] #args += ['-device', 'e1000,netdev=net0'] args += ['-device', 'virtio-net-pci,netdev=net0'] puts "ssh -p #{port} user@localhost" else #args += ["-device", "virtio-gpu-device"] if opts[:display] == DisplayMode::fullscreen # #args += ["-display", "cocoa,full-screen=on"] # # attempts: # #args += ["-display", "cocoa,full-screen=on,retina=on"] # # brew install gtk+3 sdl2 # # args += ["-display", "sdl,gl=on,full-screen=on"] # #args += ["-display", "gtk,gl=on,full-screen=on"] # #args += ["-display", "cocoa,full-screen=on"] # #args += ["-display", "cocoa,gl=es,full-screen=on"] #### TODO: try make it work with custom build # args += ["-device", "virtio-gpu-gl-pci"] # args += ["-display", "sdl,gl=on,full-screen=on"] 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"] #args += ['-nic', 'user,model=virtio-net-pci'] args += ["-device", "virtio-net,netdev=n0", "-netdev", "user,id=n0"] # macOS vmnet (shares Mac’s LAN) # -netdev vmnet-shared,id=n1 \ # -device virtio-net-pci,netdev=n1 end # 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,id=nvme0"] #args += ["-device", "nvme,serial=nvme0,drive=nvme0,bootindex=2"] if opts[:cdrom] != nil #args += ["-cdrom", opts[:cdrom]] # args += ["-device", "virtio-scsi-pci,id=scsi"] # args += ["-drive", "if=none,id=cd,format=raw,file=#{opts[:cdrom]},media=cdrom"] # args += ["-device", "scsi-cd,drive=cd,bootindex=1"] args += ["-drive", "id=cd,format=raw,file=#{opts[:cdrom]},media=cdrom"] args += ["-device", "usb-storage,drive=cd,bootindex=1"] args += ["-device", "ramfb"] # args += ["-device", "virtio-gpu-pci"] # args += ["-display", "default,show-cursor=on"] end if opts[:tpm] # brew install swtpm # swtpm socket --tpm2 --ctrl type=unixio,path=./tpm/tpm.sock --tpmstate dir=./tpm --daemon # args += ["-chardev", "socket,id=chrtpm,path=/Users/agurgul/Downloads/tpm/tpm.sock"] # args += ["-tpmdev", "emulator,id=tpm0,chardev=chrtpm"] # args += ["-device", "tpm-crb-device,tpmdev=tpm0"] args += ["-chardev", "socket,id=chrtpm,path=/Users/agurgul/Downloads/tpm/tpm.sock"] args += ["-tpmdev", "emulator,id=tpm0,chardev=chrtpm"] #args += ["-device", "tpm-tis,tpmdev=tpm0"] args += ["-device", "tpm-tis-device,tpmdev=tpm0"] # nic user,ipv6=off,model=rtl8139,mac=84:1b:77:c9:03:a6 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(' ')}" if opts[:detach] log = File.open("log.txt", "w") pid = Process.spawn(*cmd, pgroup: false, out: log, err: log) Process.detach(pid) puts "QEMU pid=#{pid}" else pid = Process.spawn(*cmd, pgroup: true, out: $stdout, err: $stderr) Process.wait(pid) status = $? puts "Exit status: #{status.exitstatus}" end end end