require "fileutils" require "ostruct" module DisplayMode def self.none 0 end def self.fullscreen 1 end def self.window 2 end def self.vnc 3 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, shell: false, ram: 2048 * 8, cpus: 1, display: DisplayMode::none, mount: { wd: nil, home: nil } } # for testing only defaults[:detach] = false defaults[:display] = DisplayMode.fullscreen defaults[:display] = DisplayMode.window # defaults[:display] = DisplayMode.none #defaults[:display] = DisplayMode.vnc 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 unless File.exist?(opts[:vars_fd]) #System.qemu_paths FileUtils.cp(System.qemu_vars_fd_path, opts[:vars_fd]) end unless File.exist?(opts[:code_fd]) FileUtils.cp(System.qemu_code_fd_path, opts[:code_fd]) end args += ["-drive", "if=pflash,format=raw,unit=0,readonly=on,file=#{opts[:code_fd]}"] args += ["-drive", "if=pflash,format=raw,unit=1,file=#{opts[:vars_fd]}"] ssh_port = nil if opts[:shell] ssh_port = rand(4000..9999) # args += ['-netdev', "user,id=net0,hostfwd=tcp:127.0.0.1:#{ssh_port}-:22"] # args += ['-device', 'virtio-net-device,netdev=net0'] # args += ['-netdev', "user,id=n0,hostfwd=tcp:127.0.0.1:#{ssh_port}-10.0.2.15:22"] # args += ['-device', 'virtio-net,netdev=n0'] # args += ['-netdev', "user,id=net0,hostfwd=tcp:127.0.0.1:#{ssh_port}-10.0.2.15:22"] # args += ['-device', 'virtio-net-device,netdev=net0"'] args += ['-nic', "user,model=virtio-net-pci,hostfwd=tcp:127.0.0.1:#{ssh_port}-:22"] puts "ssh -p #{ssh_port} user@localhost" # conf that works #args += ["-device", "virtio-net,netdev=n0", "-netdev", "user,id=n0"] end # -virtfs local,path=.,mount_tag=hostfs,security_model=passthrough,id=hostfs # mount -t 9p -o trans=virtio,version=9p2000.L hostfs /mnt # sudo mount -t 9p hostfs /home/user/Share -o trans=virtio,version=9p2000.L,uid=1000,gid=1000,msize=262144,cache=mmap # hostfs /home 9p trans=virtio,version=9p2000.L,uid=1000,gid=1000,msize=262144,cache=mmap,nofail 0 0 # hostfs /share 9p trans=virtio,version=9p2000.L,uid=1000,gid=1000,msize=262144,cache=mmap,nofail 0 0 if opts[:mount][:wd] args += ['-virtfs', 'local,path=.,mount_tag=hostfs,security_model=passthrough,id=hostfs'] end if opts[:mount][:home] #args += ['-homefs', "local,path=#{VMDATA},mount_tag=hostfs,security_model=passthrough,id=hostfs"] end if opts[:display] == DisplayMode::none port = 2222 args += ['-nographic'] args += ['-netdev', "user,id=net0,hostfwd=tcp:127.0.0.1:#{port}-:22,udp:127.0.0.1:6544-:6544=on"] #args += ['-device', 'e1000,netdev=net0'] args += ['-device', 'virtio-net-pci,netdev=net0'] puts "ssh -p #{port} user@localhost" elsif opts[:display] == DisplayMode::vnc # Note: this outputs serial on the console #args += ['-nographic'] args += ["-display", "none"] args += ["-device", "virtio-gpu-pci"] args += ["-device", "virtio-keyboard-pci"] args += ["-device", "virtio-mouse-pci"] args += ['-vnc', '127.0.0.1:0,password=on'] # tunnel ssh -L 5900:127.0.0.1:5900 user@your-host # -monitor unix:/tmp/qemu-mon,server,nowait # -vnc 127.0.0.1:0,password=on # printf 'change vnc password\nMySecret\n' | socat - UNIX-CONNECT:/tmp/qemu-mon # SASL auth (username + password) # -vnc :0,sasl # TLS (certificates, optional password) # -object tls-creds-x509,id=tls0,... # -vnc :0,tls-creds=tls0 # qemu-system-x86_64 \ # -object tls-creds-x509,id=tls0,dir=/etc/pki/qemu,endpoint=server \ # -vnc :0,tls-creds=tls0,sasl 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'] unless opts[:shell] args += ["-device", "virtio-net,netdev=n0", "-netdev", "user,id=n0"] end ### TODO: remove port = 2222 args += ['-device', 'virtio-net-pci,netdev=net0'] args += ['-netdev', "user,id=net0,hostfwd=tcp:127.0.0.1:#{port}-:22,hostfwd=udp:127.0.0.1:6544-:6544"] args += ['-virtfs', 'local,path=.,mount_tag=hostfs,security_model=passthrough,id=hostfs'] ### TODO END # 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 ["swtpm", "socket", "--tpm2", "--ctrl", "type=unixio,path=./tpm/tpm.sock", "--tpmstate", "dir=./tpm"] # 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 # TODO: Shared network on macOS # -netdev vmnet-shared,id=net0 end args += ['-monitor', 'stdio'] # 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}" if opts[:shell] System.exec_ssh(ssh_port) end else pid = Process.spawn(*cmd, pgroup: true, out: $stdout, err: $stderr) Process.wait(pid) status = $? puts "Exit status: #{status.exitstatus}" end end end ## Works on MacOS= # -monitor unix:/tmp/qemu-monitor.sock,server,nowait # nc -U /tmp/qemu-monitor.sock # instead of args += ['-monitor', 'stdio'] # 9P # sudo mount -t 9p hostfs /home/user/Share \ # -o trans=virtio,version=9p2000.L,msize=262144,cache=mmap,access=any,dfltuid=1000,dfltgid=1000 # sudo chown -hR 1000:1000 /home/user/Share