├── .gitignore ├── .github ├── FUNDING.yml └── workflows │ ├── rust.yml │ ├── docs.yml │ ├── release.yml │ └── rust-clippy.yml ├── quickget ├── cli │ ├── i18n.toml │ ├── i18n │ │ └── en │ │ │ └── quickget_rs.ftl │ ├── Cargo.toml │ └── src │ │ ├── i18n.rs │ │ ├── main.rs │ │ └── config.rs └── core │ ├── i18n.toml │ ├── src │ ├── lib.rs │ ├── i18n.rs │ ├── error.rs │ ├── data_structures.rs │ └── config_search.rs │ ├── i18n │ └── en │ │ └── quickget_core.ftl │ └── Cargo.toml ├── quickemu ├── core │ ├── i18n.toml │ ├── src │ │ ├── args.rs │ │ ├── lib.rs │ │ ├── args │ │ │ ├── io │ │ │ │ ├── mouse.rs │ │ │ │ ├── keyboard.rs │ │ │ │ ├── public_dir.rs │ │ │ │ ├── usb.rs │ │ │ │ ├── audio.rs │ │ │ │ ├── spice.rs │ │ │ │ └── display.rs │ │ │ ├── arch.rs │ │ │ ├── machine │ │ │ │ ├── ram.rs │ │ │ │ ├── tpm.rs │ │ │ │ └── boot.rs │ │ │ ├── images │ │ │ │ ├── img.rs │ │ │ │ ├── iso.rs │ │ │ │ └── disks.rs │ │ │ ├── images.rs │ │ │ ├── network │ │ │ │ └── monitor.rs │ │ │ ├── guest.rs │ │ │ ├── io.rs │ │ │ ├── machine.rs │ │ │ └── network.rs │ │ ├── i18n.rs │ │ ├── data │ │ │ ├── guest.rs │ │ │ ├── machine.rs │ │ │ ├── image.rs │ │ │ ├── display.rs │ │ │ ├── network.rs │ │ │ └── io.rs │ │ ├── data.rs │ │ ├── live_vm.rs │ │ ├── utils.rs │ │ ├── error.rs │ │ └── config.rs │ ├── Cargo.toml │ └── i18n │ │ └── en │ │ └── quickemu_core.ftl └── cli │ ├── Cargo.toml │ └── src │ └── main.rs ├── rustfmt.toml ├── docs ├── book.toml └── src │ ├── SUMMARY.md │ ├── usage │ ├── quickemu.md │ ├── installation.md │ └── quickget.md │ ├── introduction.md │ └── configuration │ └── configuration.md ├── Cargo.toml ├── README.md ├── flake.nix └── flake.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: lj3954 2 | -------------------------------------------------------------------------------- /quickget/cli/i18n.toml: -------------------------------------------------------------------------------- 1 | fallback_language = "en" 2 | 3 | [fluent] 4 | assets_dir = "i18n" 5 | -------------------------------------------------------------------------------- /quickemu/core/i18n.toml: -------------------------------------------------------------------------------- 1 | fallback_language = "en" 2 | 3 | [fluent] 4 | assets_dir = "i18n" 5 | -------------------------------------------------------------------------------- /quickget/core/i18n.toml: -------------------------------------------------------------------------------- 1 | fallback_language = "en" 2 | 3 | [fluent] 4 | assets_dir = "i18n" 5 | -------------------------------------------------------------------------------- /quickemu/core/src/args.rs: -------------------------------------------------------------------------------- 1 | mod arch; 2 | mod guest; 3 | mod images; 4 | mod io; 5 | mod machine; 6 | mod network; 7 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 200 2 | chain_width = 80 3 | fn_call_width = 80 4 | edition = "2021" 5 | fn_params_layout = "Compressed" 6 | -------------------------------------------------------------------------------- /docs/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["lj3954"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "quickemu-rs" 7 | -------------------------------------------------------------------------------- /quickemu/cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "quickemu-rs" 3 | version = "2.0.1" 4 | edition = "2021" 5 | license = "GPL-3.0 OR GPL-2.0" 6 | 7 | [dependencies] 8 | env_logger = "0.11.6" 9 | log = "0.4.25" 10 | quickemu_core = { path = "../core" } 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace.package] 2 | license = "GPL-3.0" 3 | 4 | [workspace] 5 | members = ["quickget/cli", "quickget/core", "quickemu/core", "quickemu/cli"] 6 | 7 | resolver = "2" 8 | 9 | [profile.release] 10 | strip = true 11 | lto = true 12 | opt-level = "z" 13 | codegen-units = 1 14 | panic = "abort" 15 | -------------------------------------------------------------------------------- /quickemu/core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "quickemu")] 2 | mod args; 3 | pub mod config; 4 | pub mod data; 5 | #[cfg(feature = "quickemu")] 6 | pub mod error; 7 | #[cfg(feature = "quickemu")] 8 | mod i18n; 9 | #[cfg(feature = "quickemu")] 10 | pub mod live_vm; 11 | #[cfg(feature = "quickemu")] 12 | mod utils; 13 | -------------------------------------------------------------------------------- /docs/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Introduction](./introduction.md) 4 | 5 | # Installation and Usage 6 | 7 | - [Installation](./usage/installation.md) 8 | - [Quickget](./usage/quickget.md) 9 | - [Quickemu](./usage/quickemu.md) 10 | 11 | # Configuration 12 | 13 | - [Configuration](./configuration/configuration.md) 14 | -------------------------------------------------------------------------------- /docs/src/usage/quickemu.md: -------------------------------------------------------------------------------- 1 | # Quickemu 2 | 3 | Quickemu is responsible for launching virtual machines using configuration files. 4 | 5 | Configuration files are TOML formatted, information about their usage can be found in 6 | the [configuration docs](../configuration/configuration.md). 7 | 8 | ## Usage 9 | 10 | Run `quickemu-rs` followed by a path to a configuration file. 11 | 12 | For example, 13 | ```bash 14 | quickemu-rs ubuntu-24.04-x86_64.toml 15 | ``` 16 | -------------------------------------------------------------------------------- /quickget/core/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod data_structures; 2 | 3 | #[cfg(feature = "quickget")] 4 | mod config_search; 5 | #[cfg(feature = "quickget")] 6 | mod error; 7 | #[cfg(feature = "quickget")] 8 | mod instance; 9 | #[cfg(feature = "quickget")] 10 | pub use config_search::{ConfigSearch, QuickgetConfig}; 11 | #[cfg(feature = "quickget")] 12 | pub use error::{ConfigSearchError, DLError}; 13 | #[cfg(feature = "quickget")] 14 | pub use instance::{QGDockerSource, QGDownload, QuickgetInstance}; 15 | #[cfg(feature = "quickget")] 16 | mod i18n; 17 | -------------------------------------------------------------------------------- /docs/src/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | [quickemu-rs][homepage] is a collection of two Rust programs, quickget and quickemu, each of which are designed to simplify the 4 | creation and running of Virtual Machines. This documentation specifically covers the usage of these two programs in their 5 | CLI forms, including configuration options. 6 | 7 | > quickemu-rs uses [QEMU][qemu-homepage] under the hood to allow for large amounts of configurability. 8 | 9 | [homepage]: https://github.com/lj3954/quickemu-rs 10 | [qemu-homepage]: https://www.qemu.org/ 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Quickemu-rs 2 | 3 | Create and manage macOS, Linux, and Windows virtual machines 4 | 5 | The project's documentation, including usage, installation, and configuration instructions, can be found [here](https://quickemu-rs.lj3954.dev/). 6 | 7 | ## Licensing 8 | 9 | All parts of quickget-rs are licensed under the GPLv3-only license. Full text can be found in the LICENSE-GPLv3 file. 10 | Quickemu-rs is dual licensed under GPLv2-only and GPLv3-only. This is done to allow QEMU to be statically linked with the produced binary in the future, simplifying distribution of quickemu-rs in containerized formats or where a builtin QEMU is otherwise wanted. 11 | -------------------------------------------------------------------------------- /quickemu/core/src/args/io/mouse.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | arg, 3 | data::{GuestOS, Mouse}, 4 | utils::{EmulatorArgs, QemuArg}, 5 | }; 6 | 7 | impl GuestOS { 8 | pub(crate) fn default_mouse(&self) -> Mouse { 9 | match self { 10 | GuestOS::FreeBSD | GuestOS::GhostBSD => Mouse::Usb, 11 | _ => Mouse::Tablet, 12 | } 13 | } 14 | } 15 | 16 | impl EmulatorArgs for Mouse { 17 | fn qemu_args(&self) -> impl IntoIterator { 18 | let device = match self { 19 | Self::PS2 => return vec![], 20 | Self::Usb => "usb-mouse,bus=input.0", 21 | Self::Tablet => "usb-tablet,bus=input.0", 22 | Self::Virtio => "virtio-mouse", 23 | }; 24 | vec![arg!("-device"), arg!(device)] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | strategy: 15 | matrix: 16 | include: 17 | - os: ubuntu-latest 18 | - os: ubuntu-24.04-arm 19 | - os: macos-latest 20 | runs-on: ${{ matrix.os }} 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - name: Install dependencies 25 | if: ${{ matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm' }} 26 | run: sudo apt-get update && sudo apt-get install -y libxcb-shape0-dev libxcb-xfixes0-dev 27 | 28 | - name: Build 29 | run: cargo build --verbose 30 | 31 | - name: Test 32 | run: cargo test --verbose 33 | -------------------------------------------------------------------------------- /quickget/cli/i18n/en/quickget_rs.ftl: -------------------------------------------------------------------------------- 1 | invalid-architecture = Invalid architecture: { $architecture } 2 | list-specified-os = An operating system must not be specified for list operations 3 | docker-command-failed = Failed to run docker command: { $command } 4 | 5 | unspecified-os = 6 | You must specify an operating system 7 | - Supported Operating Systems 8 | { $operating_systems } 9 | 10 | releases = Releases 11 | editions = Editions 12 | 13 | # Releases variable will be formatted within code, since it can be dynamic depending on if all releases have the same editions 14 | # This is why the plurals for releases & editions are included as separate localized values 15 | unspecified-release = 16 | You must specify a release 17 | { $releases } 18 | 19 | unspecified-edition = 20 | You must specify an edition 21 | - Editions: { $editions } 22 | -------------------------------------------------------------------------------- /quickget/cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "quickget-rs" 3 | version = "2.0.1" 4 | edition = "2021" 5 | license.workspace = true 6 | 7 | [dependencies] 8 | quickget_core = { path = "../core" } 9 | tokio = { version = "1.39.2", features = ["full"] } 10 | anyhow = "1.0.86" 11 | quickemu_core = { path = "../../quickemu/core", default-features = false } 12 | itertools = "0.13.0" 13 | clap = { version = "4.5.4", features = ["derive"] } 14 | clap-verbosity-flag = "2.2.0" 15 | reqwest = { version = "0.12.5", default-features = false, features = [ 16 | "rustls-tls", 17 | ] } 18 | indicatif = "0.17.8" 19 | serde_json = "1.0.122" 20 | csv = "1.3.0" 21 | serde = "1.0.205" 22 | i18n-embed-fl = { version = "0.9.3" } 23 | rust-embed = { version = "8.5.0" } 24 | env_logger = "0.11.8" 25 | log = "0.4.27" 26 | 27 | [dependencies.i18n-embed] 28 | version = "0.15" 29 | features = ["fluent-system", "desktop-requester"] 30 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Create and manage macOS, Linux, and Windows virtual machines with intuitive configuration"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | naersk.url = "github:nix-community/naersk"; 8 | }; 9 | 10 | outputs = inputs: with inputs; 11 | flake-utils.lib.eachDefaultSystem (system: 12 | let 13 | pkgs = (import nixpkgs) { 14 | inherit system; 15 | }; 16 | 17 | naersk' = pkgs.callPackage naersk {}; 18 | 19 | in rec { 20 | defaultPackage = naersk'.buildPackage { 21 | name = "quickemu-rs"; 22 | src = ./.; 23 | buildInputs = with pkgs; [ xorg.libxcb ]; 24 | }; 25 | 26 | devShell = pkgs.mkShell { 27 | nativeBuildInputs = with pkgs; [ rustc cargo xorg.libxcb ]; 28 | }; 29 | } 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /quickemu/cli/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, path::Path}; 2 | 3 | use quickemu_core::config::{Config, ParsedVM}; 4 | fn main() -> Result<(), Box> { 5 | env_logger::builder().filter_level(log::LevelFilter::Warn).init(); 6 | 7 | let config_file = std::env::args().nth(1).ok_or("No config file provided")?; 8 | let config = Config::parse(Path::new(&config_file)).map_err(|e| format!("Couldn't parse config: {e}"))?; 9 | 10 | let result = match config { 11 | ParsedVM::Config(config) => config.launch()?, 12 | ParsedVM::Live(_) => return Err("VM is already running".into()), 13 | }; 14 | 15 | result.warnings.iter().for_each(|warning| log::warn!("{warning}")); 16 | result 17 | .display 18 | .iter() 19 | .for_each(|display| println!(" - {}: {}", display.name, display.value)); 20 | 21 | for thread in result.threads { 22 | thread.join().expect("Couldn't join thread")?; 23 | } 24 | 25 | Ok(()) 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'docs/**' 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Setup mdBook 17 | uses: peaceiris/actions-mdbook@v2 18 | with: 19 | mdbook-version: 'latest' 20 | 21 | - run: mdbook build docs -d target 22 | 23 | - name: Upload artifact 24 | id: deployment 25 | uses: actions/upload-pages-artifact@v3 26 | with: 27 | path: ./docs/target 28 | 29 | deploy: 30 | needs: build 31 | 32 | permissions: 33 | pages: write 34 | id-token: write 35 | 36 | environment: 37 | name: github-pages 38 | url: ${{ steps.deployment.outputs.page_url }} 39 | 40 | runs-on: ubuntu-latest 41 | steps: 42 | - name: Deploy to GitHub Pages 43 | id: deployment 44 | uses: actions/deploy-pages@v4 45 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - v?[0-9]+.* 10 | 11 | jobs: 12 | create-release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: taiki-e/create-gh-release-action@v1 17 | with: 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | 20 | build: 21 | needs: create-release 22 | strategy: 23 | matrix: 24 | include: 25 | - target: aarch64-unknown-linux-gnu 26 | os: ubuntu-24.04-arm 27 | - target: x86_64-unknown-linux-gnu 28 | os: ubuntu-latest 29 | - target: universal-apple-darwin 30 | os: macos-latest 31 | runs-on: ${{ matrix.os }} 32 | steps: 33 | - uses: actions/checkout@v4 34 | 35 | - name: Install dependencies 36 | if: ${{ matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm' }} 37 | run: sudo apt-get update && sudo apt-get install -y libxcb-shape0-dev libxcb-xfixes0-dev 38 | 39 | - uses: taiki-e/upload-rust-binary-action@v1 40 | with: 41 | bin: quickemu-rs,quickget-rs 42 | archive: "quickemu-rs-${{ matrix.target }}" 43 | target: ${{ matrix.target }} 44 | token: ${{ secrets.GITHUB_TOKEN }} 45 | -------------------------------------------------------------------------------- /quickemu/core/src/i18n.rs: -------------------------------------------------------------------------------- 1 | use std::sync::LazyLock; 2 | 3 | use i18n_embed::{ 4 | fluent::{fluent_language_loader, FluentLanguageLoader}, 5 | DefaultLocalizer, DesktopLanguageRequester, LanguageLoader, Localizer, 6 | }; 7 | use rust_embed::RustEmbed; 8 | 9 | fn init(loader: &FluentLanguageLoader) { 10 | let requested_languages = DesktopLanguageRequester::requested_languages(); 11 | 12 | let localizer = DefaultLocalizer::new(loader, &Localizations); 13 | if let Err(e) = localizer.select(&requested_languages) { 14 | log::warn!("Failed to load localizations: {e}"); 15 | } 16 | } 17 | 18 | #[derive(RustEmbed)] 19 | #[folder = "i18n/"] 20 | struct Localizations; 21 | 22 | pub static LANGUAGE_LOADER: LazyLock = LazyLock::new(|| { 23 | let loader = fluent_language_loader!(); 24 | loader 25 | .load_fallback_language(&Localizations) 26 | .expect("Error while loading fallback language"); 27 | init(&loader); 28 | 29 | loader 30 | }); 31 | 32 | #[macro_export] 33 | macro_rules! fl { 34 | ($message_id:literal) => {{ 35 | i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id) 36 | }}; 37 | 38 | ($message_id:literal, $($args:expr),*) => {{ 39 | i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id, $($args), *) 40 | }}; 41 | } 42 | -------------------------------------------------------------------------------- /quickget/core/src/i18n.rs: -------------------------------------------------------------------------------- 1 | use std::sync::LazyLock; 2 | 3 | use i18n_embed::{ 4 | fluent::{fluent_language_loader, FluentLanguageLoader}, 5 | DefaultLocalizer, DesktopLanguageRequester, LanguageLoader, Localizer, 6 | }; 7 | use rust_embed::RustEmbed; 8 | 9 | fn init(loader: &FluentLanguageLoader) { 10 | let requested_languages = DesktopLanguageRequester::requested_languages(); 11 | 12 | let localizer = DefaultLocalizer::new(loader, &Localizations); 13 | if let Err(e) = localizer.select(&requested_languages) { 14 | log::warn!("Failed to load localizations: {e}"); 15 | } 16 | } 17 | 18 | #[derive(RustEmbed)] 19 | #[folder = "i18n/"] 20 | struct Localizations; 21 | 22 | pub static LANGUAGE_LOADER: LazyLock = LazyLock::new(|| { 23 | let loader = fluent_language_loader!(); 24 | loader 25 | .load_fallback_language(&Localizations) 26 | .expect("Error while loading fallback language"); 27 | init(&loader); 28 | 29 | loader 30 | }); 31 | 32 | #[macro_export] 33 | macro_rules! fl { 34 | ($message_id:literal) => {{ 35 | i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id) 36 | }}; 37 | 38 | ($message_id:literal, $($args:expr),*) => {{ 39 | i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id, $($args), *) 40 | }}; 41 | } 42 | -------------------------------------------------------------------------------- /quickget/core/i18n/en/quickget_core.ftl: -------------------------------------------------------------------------------- 1 | # Config search errors 2 | failed-cache-dir = Failed to determine system cache directory 3 | invalid-cache-dir = Cache directory { $dir } does not exist 4 | invalid-system-time = Invalid system time: { $err } 5 | failed-cache-file = Unable to interact with cache file: { $err } 6 | failed-download = Failed to download cache file: { $err } 7 | failed-json = Failed to serialize JSON data: { $err } 8 | required-os = An OS must be specified before searching for releases, editions, or architectures 9 | required-release = A release is required before searching for editions 10 | required-edition = An edition is required before selecting a config 11 | invalid-os = No OS matching { $os } was found 12 | invalid-release = No release { $rel } found for { $os } 13 | invalid-edition = No edition { $edition } found 14 | invalid-arch = Architecture { $arch } not found including other parameters 15 | no-editions = No editions are available for the specified release 16 | 17 | # Download errors 18 | unsupported-source = A source does not currently exist for { $os } 19 | invalid-vm-name = Invalid VM name { $vm_name } 20 | config-file-error = Unable to write to config file: { $err } 21 | config-data-error = Unable to serialize config data: { $err } 22 | download-error = File { $file } was not successfully downloaded 23 | invalid-checksum = Invalid checksum: { $cs } 24 | failed-validation = Checksums did not match. Expected { $expected }, got { $actual } 25 | dir-exists = VM Directory { $dir } already exists 26 | -------------------------------------------------------------------------------- /docs/src/usage/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Arch Linux 4 | 5 | quickemu-rs is officially maintained on the AUR; you can install it with your favourite AUR helper. 6 | 7 | For instance, 8 | ```bash 9 | paru -S quickemu-rs 10 | ``` 11 | 12 | ## Dependencies 13 | 14 | quickemu-rs depends on QEMU, which must be compiled with GTK, SDL, SPICE, and VirtFS support. 15 | By default, smartcard support is also required, and the minimum QEMU version is 8.1.0. 16 | 17 | If you are [building from source](./installation.md#compilation-from-source), you can optionally disable the `smartcard_args` and `qemu_8_1` features, 18 | which will remove these requirements. 19 | 20 | ## Binaries 21 | 22 | Pre-built binaries are available for macOS and GNU/Linux on the [releases page][releases]. 23 | To install, download the archive for your platform, extract it, and copy the binaries to a directory in your `$PATH`, 24 | such as `~/.local/bin`. 25 | 26 | ## Compilation from source 27 | 28 | Alternatively, you can manually compile from source. To do so, you will need to have Rust installed. 29 | For more information on how to install Rust, see the [official Rust website][rust-install]. 30 | 31 | Once you have installed Rust, you can clone the repository and build the project: 32 | ```bash 33 | git clone https://github.com/lj3954/quickemu-rs.git 34 | cd quickemu-rs 35 | cargo build --release 36 | ``` 37 | 38 | This will compile the 2 binaries into the `target/release` directory. Then, you can copy the binaries into a directory in your `$PATH`. 39 | 40 | [releases]: https://github.com/lj3954/quickemu-rs/releases 41 | [rust-install]: https://www.rust-lang.org/tools/install 42 | -------------------------------------------------------------------------------- /docs/src/usage/quickget.md: -------------------------------------------------------------------------------- 1 | # Quickget 2 | 3 | Quickget is responsible for downloading operating system images from pre-generated URLs, and creating sensible 4 | configurations which can be passed into quickemu. 5 | 6 | > Since all available operating systems are fetched from the internet, quickget may be slow on its first launch 7 | each day. Configurations are cached and will be refreshed if quickget hasn't been run since UTC midnight. 8 | 9 | ## Usage 10 | 11 | Running `quickget-rs` without any arguments will list all available operating systems. 12 | Then, you can pass in an operating system to list all available releases and (if applicable) editions. 13 | 14 | Following that, you can easily download an operating system and create a configuration by passing in the 15 | operating system, release, and edition (if applicable). 16 | 17 | For example, to create a VM running Kubuntu 24.04 LTS: 18 | ```bash 19 | quickget-rs kubuntu 24.04 20 | ``` 21 | 22 | ### CLI arguments 23 | 24 | | Argument | Description | 25 | |----------|-------------| 26 | | `-h`, `--help` | Print help message | 27 | | `--verbose` | Enable verbose output | 28 | | `-r`, `--refresh` | Force configuration data to be refreshed | 29 | | `-a`, `--arch` | Specify the architecture of operating system you want to download. By default, quickget will select your system's architecture if possible | 30 | | `-l`, `--list` | List all available operating systems, releases, and editions. By default, plain text will be printed. Passing `csv` or `json` will modify the formatting. This is mainly here for backwards compatibility | 31 | 32 | 33 | -------------------------------------------------------------------------------- /quickemu/core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "quickemu_core" 3 | edition = "2021" 4 | version = "2.0.1" 5 | license = "GPL-3.0 OR GPL-2.0" 6 | 7 | [dependencies] 8 | derive_more = { version = "2.0.0", features = ["display", "as_ref", "from"] } 9 | dirs = { version = "5.0.1", optional = true } 10 | display-info = { version = "0.5.1", optional = true } 11 | log = { version = "0.4.21", optional = true } 12 | num_cpus = { version = "1.16.0", optional = true } 13 | raw-cpuid = { version = "11.0.2", optional = true } 14 | serde = { version = "1.0.201", features = ["derive"] } 15 | sysinfo = { version = "0.30.10", default-features = false, optional = true } 16 | toml = { version = "0.8.12", optional = true } 17 | which = { version = "6.0.1", optional = true } 18 | itertools = "0.13.0" 19 | size = { version = "0.4.1", optional = true } 20 | memfd-exec = { version = "0.2.1", optional = true } 21 | serde_json = { version = "1.0.137", optional = true } 22 | strum = { version = "0.26.3", features = ["derive"] } 23 | i18n-embed-fl = { version = "0.9.3", optional = true } 24 | rust-embed = { version = "8.5.0", optional = true } 25 | 26 | [dependencies.i18n-embed] 27 | optional = true 28 | version = "0.15" 29 | features = ["fluent-system", "desktop-requester"] 30 | 31 | [features] 32 | default = ["quickemu", "display_resolution", "smartcard_args", "qemu_8_1"] 33 | 34 | quickemu = [ 35 | "dirs", 36 | "log", 37 | "num_cpus", 38 | "raw-cpuid", 39 | "sysinfo", 40 | "toml", 41 | "which", 42 | "i18n-embed-fl", 43 | "i18n-embed", 44 | "rust-embed", 45 | "size", 46 | "serde_json", 47 | ] 48 | 49 | display_resolution = ["quickemu", "display-info"] 50 | smartcard_args = ["quickemu"] 51 | 52 | qemu_8_1 = ["quickemu"] 53 | 54 | inbuilt_commands = ["qemu_8_1", "smartcard_args", "memfd-exec"] 55 | -------------------------------------------------------------------------------- /quickget/core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "quickget_core" 3 | edition = "2021" 4 | version = "2.0.1" 5 | license.workspace = true 6 | 7 | [dependencies] 8 | log = { version = "0.4.21", optional = true } 9 | derive_more = { version = "1.0.0", features = ["from"] } 10 | dirs = { version = "5.0.1", optional = true } 11 | quickemu_core = { path = "../../quickemu/core", default-features = false } 12 | reqwest = { version = "0.12.4", optional = true, default-features = false, features = [ 13 | "rustls-tls", 14 | ] } 15 | serde_json = { version = "1.0.117", optional = true } 16 | serde = "1.0.202" 17 | toml = { version = "0.8.13", optional = true } 18 | which = { version = "6.0.1", optional = true } 19 | num_cpus = { version = "1.16.0", optional = true } 20 | sysinfo = { version = "0.31.2", optional = true } 21 | md-5 = { version = "0.10.6", optional = true } 22 | sha2 = { version = "0.10.8", optional = true } 23 | sha1 = { version = "0.10.6", optional = true } 24 | bzip2 = { version = "0.4.4", optional = true } 25 | flate2 = { version = "1.0.31", optional = true } 26 | liblzma = { version = "0.3.3", optional = true } 27 | zstd = { version = "0.13.1", optional = true } 28 | size = "0.4.1" 29 | i18n-embed-fl = { version = "0.9.3", optional = true } 30 | rust-embed = { version = "8.5.0", optional = true } 31 | 32 | [dependencies.i18n-embed] 33 | optional = true 34 | version = "0.15" 35 | features = ["fluent-system", "desktop-requester"] 36 | 37 | [features] 38 | default = ["quickget"] 39 | quickget = [ 40 | "dirs", 41 | "reqwest", 42 | "serde_json", 43 | "toml", 44 | "which", 45 | "num_cpus", 46 | "sysinfo", 47 | "md-5", 48 | "sha2", 49 | "sha1", 50 | "bzip2", 51 | "flate2", 52 | "liblzma", 53 | "zstd", 54 | "i18n-embed-fl", 55 | "i18n-embed", 56 | "rust-embed", 57 | "log", 58 | ] 59 | -------------------------------------------------------------------------------- /.github/workflows/rust-clippy.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # rust-clippy is a tool that runs a bunch of lints to catch common 6 | # mistakes in your Rust code and help improve your Rust code. 7 | # More details at https://github.com/rust-lang/rust-clippy 8 | # and https://rust-lang.github.io/rust-clippy/ 9 | 10 | name: rust-clippy analyze 11 | 12 | on: 13 | push: 14 | branches: [ "main" ] 15 | pull_request: 16 | # The branches below must be a subset of the branches above 17 | branches: [ "main" ] 18 | schedule: 19 | - cron: '31 17 * * 3' 20 | 21 | jobs: 22 | rust-clippy-analyze: 23 | name: Run rust-clippy analyzing 24 | runs-on: ubuntu-latest 25 | permissions: 26 | contents: read 27 | security-events: write 28 | actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status 29 | steps: 30 | - name: Checkout code 31 | uses: actions/checkout@v4 32 | 33 | - name: Install Rust toolchain 34 | uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af #@v1 35 | with: 36 | profile: minimal 37 | toolchain: stable 38 | components: clippy 39 | override: true 40 | 41 | - name: Install required cargo 42 | run: cargo install clippy-sarif sarif-fmt 43 | 44 | - name: Run rust-clippy 45 | run: 46 | cargo clippy 47 | --all-features 48 | --message-format=json | clippy-sarif | tee rust-clippy-results.sarif | sarif-fmt 49 | continue-on-error: true 50 | 51 | - name: Upload analysis results to GitHub 52 | uses: github/codeql-action/upload-sarif@v1 53 | with: 54 | sarif_file: rust-clippy-results.sarif 55 | wait-for-processing: true 56 | -------------------------------------------------------------------------------- /quickemu/core/src/data/guest.rs: -------------------------------------------------------------------------------- 1 | use derive_more::Display; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Display, Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] 5 | #[serde(tag = "os")] 6 | #[serde(rename_all = "snake_case")] 7 | pub enum GuestOS { 8 | #[serde(alias = "Linux")] 9 | #[default] 10 | Linux, 11 | #[display("Linux (Old)")] 12 | #[serde(alias = "LinuxOld")] 13 | LinuxOld, 14 | #[serde(alias = "Windows")] 15 | Windows, 16 | #[serde(alias = "WindowsServer", alias = "Windows Server")] 17 | WindowsServer, 18 | #[serde(rename = "macos", alias = "macOS")] 19 | MacOS { release: MacOSRelease }, 20 | #[serde(rename = "freebsd", alias = "FreeBSD")] 21 | FreeBSD, 22 | #[serde(rename = "ghostbsd", alias = "GhostBSD")] 23 | GhostBSD, 24 | #[serde(rename = "bsd", alias = "BSD")] 25 | GenericBSD, 26 | #[serde(rename = "freedos", alias = "FreeDOS")] 27 | FreeDOS, 28 | #[serde(alias = "Haiku")] 29 | Haiku, 30 | #[serde(alias = "Solaris")] 31 | Solaris, 32 | #[serde(rename = "kolibrios", alias = "KolibriOS", alias = "Kolibri OS")] 33 | KolibriOS, 34 | #[serde(rename = "reactos", alias = "ReactOS")] 35 | ReactOS, 36 | #[serde(alias = "Batocera")] 37 | Batocera, 38 | } 39 | 40 | #[derive(Display, Debug, PartialEq, Clone, Copy, PartialOrd, Serialize, Deserialize)] 41 | pub enum MacOSRelease { 42 | #[serde(alias = "highsierra", alias = "High Sierra", alias = "10.13")] 43 | HighSierra, 44 | #[serde(alias = "mojave", alias = "10.14")] 45 | Mojave, 46 | #[serde(alias = "catalina", alias = "10.15")] 47 | Catalina, 48 | #[serde(alias = "bigsur", alias = "Big Sur", alias = "11")] 49 | BigSur, 50 | #[serde(alias = "monterey", alias = "12")] 51 | Monterey, 52 | #[serde(alias = "ventura", alias = "13")] 53 | Ventura, 54 | #[serde(alias = "sonoma", alias = "14")] 55 | Sonoma, 56 | #[serde(alias = "sequoia", alias = "15")] 57 | Sequoia, 58 | } 59 | -------------------------------------------------------------------------------- /quickemu/core/src/args/arch.rs: -------------------------------------------------------------------------------- 1 | use crate::{data::Arch, error::Warning}; 2 | use raw_cpuid::{CpuId, CpuIdReader}; 3 | 4 | #[cfg(target_os = "linux")] 5 | use std::path::Path; 6 | 7 | impl Arch { 8 | #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] 9 | pub(crate) fn enable_hw_virt(&self) -> Result { 10 | if !self.matches_host() { 11 | return Ok(false); 12 | } 13 | #[cfg(target_arch = "x86_64")] 14 | #[cfg(not(target_os = "linux"))] 15 | { 16 | let cpuid = CpuId::new(); 17 | let has_vmx = cpuid.get_feature_info().map_or(true, |f| f.has_vmx()); 18 | let has_svm = cpuid 19 | .get_extended_processor_and_feature_identifiers() 20 | .map_or(true, |f| f.has_svm()); 21 | if !has_vmx && !has_svm { 22 | return Err(Warning::HwVirt(query_virt_type(cpuid))); 23 | } 24 | } 25 | #[cfg(target_os = "linux")] 26 | if !Path::new("/dev/kvm").exists() { 27 | #[cfg(target_arch = "x86_64")] 28 | return Err(Warning::HwVirt(query_virt_type(CpuId::new()))); 29 | #[cfg(not(target_arch = "x86_64"))] 30 | return Err(Warning::HwVirt("")); 31 | } 32 | Ok(true) 33 | } 34 | #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] 35 | fn matches_host(&self) -> bool { 36 | match self { 37 | Self::X86_64 { .. } => cfg!(target_arch = "x86_64"), 38 | Self::AArch64 { .. } => cfg!(target_arch = "aarch64"), 39 | Self::Riscv64 { .. } => cfg!(target_arch = "riscv64"), 40 | } 41 | } 42 | } 43 | 44 | #[cfg(target_arch = "x86_64")] 45 | fn query_virt_type(cpuid_reader: CpuId) -> &'static str { 46 | cpuid_reader.get_vendor_info().map_or("", |v| match v.as_str() { 47 | "GenuineIntel" => " (VT-x)", 48 | "AuthenticAMD" => " (AMD-V)", 49 | _ => "", 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /quickemu/core/src/args/machine/ram.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use size::Size; 4 | 5 | use crate::{ 6 | arg, 7 | data::{GuestOS, Machine}, 8 | error::{Error, Warning}, 9 | oarg, 10 | utils::{ArgDisplay, EmulatorArgs, QemuArg}, 11 | }; 12 | 13 | pub struct Ram { 14 | ram: Size, 15 | total_ram: Size, 16 | free_ram: Size, 17 | } 18 | 19 | const MIN_MACOS_WINDOWS_RAM: i64 = 4 * size::consts::GiB; 20 | 21 | impl Machine { 22 | pub fn ram_args(&self, guest: GuestOS) -> Result<(Ram, Option), Error> { 23 | let mut warning = None; 24 | 25 | let system = sysinfo::System::new_with_specifics(sysinfo::RefreshKind::new().with_memory(sysinfo::MemoryRefreshKind::new().with_ram())); 26 | let free_ram = Size::from_bytes(system.available_memory()); 27 | let total_ram = Size::from_bytes(system.total_memory()); 28 | 29 | let mut ram = self.ram.map_or_else(|| match total_ram.bytes() / size::consts::GiB { 30 | 128.. => 32, 31 | 64.. => 16, 32 | 16.. => 8, 33 | 8.. => 4, 34 | 4.. => 2, 35 | _ => 1, 36 | } * size::consts::GiB, |ram| ram as i64); 37 | 38 | if ram < MIN_MACOS_WINDOWS_RAM { 39 | if self.ram.is_some() { 40 | warning = Some(Warning::InsufficientRamConfiguration(total_ram, guest)); 41 | } else if total_ram.bytes() < MIN_MACOS_WINDOWS_RAM { 42 | return Err(Error::InsufficientRam(total_ram, guest)); 43 | } else { 44 | ram = MIN_MACOS_WINDOWS_RAM; 45 | } 46 | } 47 | 48 | let ram = Size::from_bytes(ram); 49 | 50 | Ok((Ram { ram, total_ram, free_ram }, warning)) 51 | } 52 | } 53 | 54 | impl EmulatorArgs for Ram { 55 | fn display(&self) -> impl IntoIterator { 56 | Some(ArgDisplay { 57 | name: Cow::Borrowed("RAM"), 58 | value: Cow::Owned(format!("{} ({} / {} available)", self.ram, self.free_ram, self.total_ram)), 59 | }) 60 | } 61 | fn qemu_args(&self) -> impl IntoIterator { 62 | [arg!("-m"), oarg!(format!("{}b", self.ram.bytes()))] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /quickget/cli/src/i18n.rs: -------------------------------------------------------------------------------- 1 | use std::sync::LazyLock; 2 | 3 | use i18n_embed::{ 4 | fluent::{fluent_language_loader, FluentLanguageLoader}, 5 | DefaultLocalizer, DesktopLanguageRequester, LanguageLoader, Localizer, 6 | }; 7 | use rust_embed::RustEmbed; 8 | 9 | fn init(loader: &FluentLanguageLoader) { 10 | let requested_languages = DesktopLanguageRequester::requested_languages(); 11 | 12 | let localizer = DefaultLocalizer::new(loader, &Localizations); 13 | if let Err(e) = localizer.select(&requested_languages) { 14 | log::warn!("Failed to load localizations: {e}"); 15 | } 16 | } 17 | 18 | #[derive(RustEmbed)] 19 | #[folder = "i18n/"] 20 | struct Localizations; 21 | 22 | pub static LANGUAGE_LOADER: LazyLock = LazyLock::new(|| { 23 | let loader = fluent_language_loader!(); 24 | loader 25 | .load_fallback_language(&Localizations) 26 | .expect("Error while loading fallback language"); 27 | init(&loader); 28 | 29 | loader 30 | }); 31 | 32 | #[macro_export] 33 | macro_rules! fl { 34 | ($message_id:literal) => {{ 35 | i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id) 36 | }}; 37 | 38 | ($message_id:literal, $($args:expr),*) => {{ 39 | i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id, $($args), *) 40 | }}; 41 | } 42 | 43 | #[macro_export] 44 | macro_rules! fl_bail { 45 | ($message_id:literal) => {{ 46 | anyhow::bail!(i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id)) 47 | }}; 48 | 49 | ($message_id:literal, $($args:expr),*) => {{ 50 | anyhow::bail!(i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id, $($args), *)) 51 | }}; 52 | } 53 | 54 | #[macro_export] 55 | macro_rules! fl_ensure { 56 | ($condition:expr, $message_id:literal) => {{ 57 | anyhow::ensure!( 58 | $condition, 59 | i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id) 60 | ) 61 | }}; 62 | ($condition:expr, $message_id:literal, $($args:expr),*) => {{ 63 | anyhow::ensure!( 64 | $condition, 65 | i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id, $($args), *) 66 | ) 67 | }}; 68 | } 69 | -------------------------------------------------------------------------------- /quickemu/core/src/args/io/keyboard.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | arg, 3 | data::{GuestOS, Keyboard, KeyboardLayout}, 4 | utils::{EmulatorArgs, QemuArg}, 5 | }; 6 | 7 | impl GuestOS { 8 | pub(crate) fn default_keyboard(&self) -> Keyboard { 9 | match self { 10 | GuestOS::ReactOS => Keyboard::PS2, 11 | _ => Keyboard::Usb, 12 | } 13 | } 14 | } 15 | 16 | impl EmulatorArgs for Keyboard { 17 | fn qemu_args(&self) -> impl IntoIterator { 18 | let device = match self { 19 | Self::PS2 => return vec![], 20 | Self::Usb => "usb-kbd,bus=input.0", 21 | Self::Virtio => "virtio-keyboard", 22 | }; 23 | vec![arg!("-device"), arg!(device)] 24 | } 25 | } 26 | 27 | impl EmulatorArgs for KeyboardLayout { 28 | fn qemu_args(&self) -> impl IntoIterator { 29 | let layout = match self { 30 | Self::Arabic => "ar", 31 | Self::SwissGerman => "de-ch", 32 | Self::Spanish => "es", 33 | Self::Faroese => "fo", 34 | Self::FrenchCanadian => "fr-ca", 35 | Self::Hungarian => "hu", 36 | Self::Japanese => "ja", 37 | Self::Macedonian => "mk", 38 | Self::Norwegian => "no", 39 | Self::BrazilianPortuguese => "pt-br", 40 | Self::Swedish => "sv", 41 | Self::Danish => "da", 42 | Self::BritishEnglish => "en-gb", 43 | Self::Estonian => "et", 44 | Self::French => "fr", 45 | Self::SwissFrench => "fr-ch", 46 | Self::Icelandic => "is", 47 | Self::Lithuanian => "lt", 48 | Self::Dutch => "nl", 49 | Self::Polish => "pl", 50 | Self::Russian => "ru", 51 | Self::Thai => "th", 52 | Self::German => "de", 53 | Self::AmericanEnglish => "en-us", 54 | Self::Finnish => "fi", 55 | Self::BelgianFrench => "fr-be", 56 | Self::Croatian => "hr", 57 | Self::Italian => "it", 58 | Self::Latvian => "lv", 59 | Self::NorwegianBokmal => "nb", 60 | Self::Portuguese => "pt", 61 | Self::Slovenian => "sl", 62 | Self::Turkish => "tr", 63 | }; 64 | vec![arg!("-k"), arg!(layout)] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /quickemu/core/i18n/en/quickemu_core.ftl: -------------------------------------------------------------------------------- 1 | # Config errors 2 | read-config-error = Could not read config file: { $err } 3 | parse-config-error = Could not parse config file: { $err } 4 | 5 | # Live VM errors 6 | failed-live-vm-de = Failed to deserialize live VM data: { $err } 7 | failed-del-live-file = Failed to delete inactive live VM status file: { $err } 8 | failed-vm-kill = Failed to kill running VM: { $err } 9 | 10 | # Monitor errors 11 | no-monitor-available = No monitor is enabled. 12 | failed-monitor-write = Could not write to the monitor: { $err } 13 | failed-monitor-read = Could not read from thet monitor: { $err } 14 | 15 | # Generic Errors 16 | macos-cpu-instructions = CPU does not support a necessary instruction for this macOS release: { $instruction }. 17 | unavailable-port = Requested port { $port } is not available. 18 | insufficient-ram = System RAM { $ram } is insufficient for { $guest } VMs. 19 | sound-usb-conflict = USB Audio requires the XHCI USB controller. 20 | which-binary = Could not find binary: { $err } 21 | failed-launch = Failed to launch { $bin }: { $err } 22 | non-x86-bios = Legacy boot is only supported on x86_64. 23 | riscv64-boot = Could not find riscv64 bootloader 24 | efi-firmware = Could not find EFI firmware 25 | failed-ovmf-copy = Could not copy OVMF vars into VM directory: { $err } 26 | unsupported-boot-combination = Specified architecture and boot type are not compatible 27 | no-viewer = Could not find viewer { $viewer_bin } 28 | no-qemu = Could not find qemu binary { $qemu_bin } 29 | failed-disk-creation = Could not create disk image: { $err } 30 | disk-used = Failed to get write lock on disk { $disk }. Ensure that it is not already in use. 31 | failed-qemu-img-deserialization = Could not deserialize qemu-img info: { $err } 32 | no-mac-bootloader = Could not find macOS bootloader in VM directory 33 | nonexistent-image = Requested to mount image { $img }, but it does not exist. 34 | monitor-command-failed = Could not send command to monitor: { $err } 35 | failed-live-vm-se = Failed to serialize live VM data: { $err } 36 | 37 | # Warnings 38 | macos-core-power-two = macOS guests may not boot witwh core counts that are not powers of two. Recommended rounding: { $recommended }. 39 | software-virt-fallback = Hardware virtualization{ $virt_branding } is not enabled on your CPU. Falling back to software virtualization, performance will be degraded 40 | audio-backend-unavailable = Sound was requested, but no audio backend could be detected. 41 | insufficient-ram-configuration = The specified amount of RAM ({ $ram }) is insufficient for { $guest }. Performance issues may arise 42 | -------------------------------------------------------------------------------- /quickemu/core/src/data.rs: -------------------------------------------------------------------------------- 1 | pub mod display; 2 | pub mod guest; 3 | pub mod image; 4 | pub mod io; 5 | pub mod machine; 6 | pub mod network; 7 | 8 | pub use display::*; 9 | pub use guest::*; 10 | pub use image::*; 11 | pub use io::*; 12 | pub use machine::*; 13 | pub use network::*; 14 | 15 | use serde::{de, Deserialize}; 16 | use std::fmt; 17 | 18 | pub fn is_default(input: &T) -> bool { 19 | input == &T::default() 20 | } 21 | pub fn is_true(input: &bool) -> bool { 22 | *input 23 | } 24 | 25 | fn parse_size(value: &str) -> Result { 26 | let mut chars = value.chars().rev(); 27 | let mut unit_char = chars.next(); 28 | if unit_char == Some('B') { 29 | unit_char = chars.next(); 30 | } 31 | let unit_char = unit_char.ok_or_else(|| de::Error::custom("No unit type was specified"))?; 32 | let size = match unit_char { 33 | 'K' => 1u64 << 10, 34 | 'M' => 1 << 20, 35 | 'G' => 1 << 30, 36 | 'T' => 1 << 40, 37 | _ => return Err(de::Error::custom("Unexpected unit type")), 38 | } as f64; 39 | 40 | let rem: String = chars.rev().collect(); 41 | let size_f: f64 = rem.parse().map_err(de::Error::custom)?; 42 | Ok((size_f * size) as u64) 43 | } 44 | 45 | struct SizeUnit; 46 | impl de::Visitor<'_> for SizeUnit { 47 | type Value = Option; 48 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 49 | formatter.write_str("a string (ending in a size unit, e.g. M, G, T) or a number (in bytes)") 50 | } 51 | fn visit_str(self, value: &str) -> Result 52 | where 53 | E: de::Error, 54 | { 55 | parse_size(value).map(Some) 56 | } 57 | fn visit_i64(self, value: i64) -> Result 58 | where 59 | E: serde::de::Error, 60 | { 61 | Ok(Some(value.try_into().map_err(serde::de::Error::custom)?)) 62 | } 63 | } 64 | pub fn deserialize_size<'de, D>(deserializer: D) -> Result, D::Error> 65 | where 66 | D: serde::Deserializer<'de>, 67 | { 68 | deserializer.deserialize_any(SizeUnit) 69 | } 70 | 71 | // This is a workaround to an issue in upstream serde 72 | // https://github.com/serde-rs/serde/issues/1626 73 | // Preallocation is non-functional when a format isn't specified with this workaround 74 | pub fn default_if_empty<'de, D, T>(deserializer: D) -> Result 75 | where 76 | D: serde::Deserializer<'de>, 77 | T: serde::Deserialize<'de> + Default, 78 | { 79 | let opt = Option::deserialize(deserializer)?; 80 | Ok(opt.unwrap_or_default()) 81 | } 82 | -------------------------------------------------------------------------------- /quickemu/core/src/args/images/img.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, ffi::OsString, path::Path}; 2 | 3 | use crate::{ 4 | arg, 5 | data::{GuestOS, Images}, 6 | error::Error, 7 | oarg, 8 | utils::{ArgDisplay, EmulatorArgs, QemuArg}, 9 | }; 10 | 11 | impl<'a> Images { 12 | pub(crate) fn img_args(&'a self, installed: bool, vm_dir: &Path, guest: GuestOS) -> Result, Error> { 13 | let images = self 14 | .img 15 | .iter() 16 | .filter(|img| img.always_mount || !installed) 17 | .map(|img| { 18 | if img.path.is_absolute() { 19 | Cow::Borrowed(img.path.as_path()) 20 | } else { 21 | Cow::Owned(vm_dir.join(&img.path)) 22 | } 23 | }) 24 | .map(|img| { 25 | if !img.exists() { 26 | return Err(Error::NonexistentImage(img.display().to_string())); 27 | } 28 | Ok(img) 29 | }) 30 | .collect::, _>>()?; 31 | 32 | Ok(ImgArgs { images, guest }) 33 | } 34 | } 35 | 36 | pub(crate) struct ImgArgs<'a> { 37 | images: Vec>, 38 | guest: GuestOS, 39 | } 40 | 41 | impl EmulatorArgs for ImgArgs<'_> { 42 | fn display(&self) -> impl IntoIterator { 43 | self.images.iter().map(|img| ArgDisplay { 44 | name: Cow::Borrowed("IMG"), 45 | value: Cow::Owned(img.display().to_string()), 46 | }) 47 | } 48 | 49 | fn qemu_args(&self) -> impl IntoIterator { 50 | let first = self.images.first().map(|first| { 51 | let first_id = match self.guest { 52 | GuestOS::MacOS { .. } => "RecoveryImage", 53 | _ => "BootDisk", 54 | }; 55 | img_args(first, first_id, self.guest) 56 | }); 57 | 58 | let rest = self 59 | .images 60 | .iter() 61 | .skip(1) 62 | .enumerate() 63 | .flat_map(|(i, img)| img_args(img, &format!("Image{i}"), self.guest)); 64 | 65 | std::iter::once(first).flatten().flatten().chain(rest) 66 | } 67 | } 68 | 69 | fn img_args(img: &Path, id: &str, guest: GuestOS) -> [QemuArg; 4] { 70 | let mut drive_arg = OsString::from("id="); 71 | drive_arg.push(id); 72 | drive_arg.push(",if=none,format=raw,file="); 73 | drive_arg.push(img); 74 | 75 | let mut device_arg = OsString::from(match guest { 76 | GuestOS::MacOS { .. } => "ide-hd,bus=ahci.1,drive=", 77 | _ => "virtio-blk-pci,drive=", 78 | }); 79 | device_arg.push(id); 80 | 81 | [arg!("-device"), oarg!(device_arg), arg!("-drive"), oarg!(drive_arg)] 82 | } 83 | -------------------------------------------------------------------------------- /quickemu/core/src/args/io/public_dir.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, 3 | ffi::{OsStr, OsString}, 4 | path::Path, 5 | }; 6 | 7 | use crate::{ 8 | arg, 9 | data::GuestOS, 10 | oarg, 11 | utils::{ArgDisplay, EmulatorArgs, QemuArg}, 12 | }; 13 | 14 | pub(crate) struct PublicDirArgs<'a> { 15 | path: &'a Path, 16 | guest: GuestOS, 17 | } 18 | 19 | impl<'a> PublicDirArgs<'a> { 20 | pub(crate) fn new(path: &'a Path, guest: GuestOS) -> Self { 21 | Self { path, guest } 22 | } 23 | } 24 | 25 | impl EmulatorArgs for PublicDirArgs<'_> { 26 | fn display(&self) -> impl IntoIterator { 27 | let mut display = Vec::new(); 28 | if let GuestOS::MacOS { .. } = self.guest { 29 | display.push(ArgDisplay { 30 | name: Cow::Borrowed("WebDAV (Guest)"), 31 | value: Cow::Borrowed("first, build spice-webdavd (https://gitlab.gnome.org/GNOME/phodav/-/merge_requests/24)"), 32 | }); 33 | } 34 | display.push(ArgDisplay { 35 | name: Cow::Borrowed("WebDAV (Guest)"), 36 | value: Cow::Borrowed("dav://localhost:9843/"), 37 | }); 38 | 39 | match self.guest { 40 | GuestOS::MacOS { .. } => { 41 | if self.path.metadata().is_ok_and(|m| m.permissions().readonly()) { 42 | display.push(ArgDisplay { 43 | name: Cow::Borrowed("9P (Host)"), 44 | value: Cow::Owned(format!("`sudo chmod -r 777 {}`", self.path.display())), 45 | }); 46 | } 47 | display.push(ArgDisplay { 48 | name: Cow::Borrowed("9P (Guest)"), 49 | value: Cow::Borrowed("sudo mount_9p Public-quickemu ~/Public"), 50 | }); 51 | } 52 | GuestOS::Linux | GuestOS::LinuxOld => { 53 | display.push(ArgDisplay { 54 | name: Cow::Borrowed("9P (Guest)"), 55 | value: Cow::Borrowed("`sudo mount -t 9p -o trans=virtio,version=9p2000.L,msize=104857600 Public-quickemu ~/Public`"), 56 | }); 57 | } 58 | _ => (), 59 | } 60 | 61 | display 62 | } 63 | fn qemu_args(&self) -> impl IntoIterator { 64 | if let GuestOS::Windows | GuestOS::WindowsServer = self.guest { 65 | return vec![]; 66 | } 67 | let mut fs = OsString::from("local,id=fsdev0,path="); 68 | fs.push(self.path); 69 | fs.push(",security_model=mapped-xattr"); 70 | 71 | let device = OsStr::new("virtio-9p-pci,fsdev=fsdev0,mount_tag=Public-quickemu"); 72 | vec![arg!("-fsdev"), oarg!(fs), arg!("-device"), arg!(device)] 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "naersk": { 22 | "inputs": { 23 | "nixpkgs": "nixpkgs" 24 | }, 25 | "locked": { 26 | "lastModified": 1745925850, 27 | "narHash": "sha256-cyAAMal0aPrlb1NgzMxZqeN1mAJ2pJseDhm2m6Um8T0=", 28 | "owner": "nix-community", 29 | "repo": "naersk", 30 | "rev": "38bc60bbc157ae266d4a0c96671c6c742ee17a5f", 31 | "type": "github" 32 | }, 33 | "original": { 34 | "owner": "nix-community", 35 | "repo": "naersk", 36 | "type": "github" 37 | } 38 | }, 39 | "nixpkgs": { 40 | "locked": { 41 | "lastModified": 1746576598, 42 | "narHash": "sha256-FshoQvr6Aor5SnORVvh/ZdJ1Sa2U4ZrIMwKBX5k2wu0=", 43 | "owner": "NixOS", 44 | "repo": "nixpkgs", 45 | "rev": "b3582c75c7f21ce0b429898980eddbbf05c68e55", 46 | "type": "github" 47 | }, 48 | "original": { 49 | "owner": "NixOS", 50 | "ref": "nixpkgs-unstable", 51 | "repo": "nixpkgs", 52 | "type": "github" 53 | } 54 | }, 55 | "nixpkgs_2": { 56 | "locked": { 57 | "lastModified": 1746663147, 58 | "narHash": "sha256-Ua0drDHawlzNqJnclTJGf87dBmaO/tn7iZ+TCkTRpRc=", 59 | "owner": "NixOS", 60 | "repo": "nixpkgs", 61 | "rev": "dda3dcd3fe03e991015e9a74b22d35950f264a54", 62 | "type": "github" 63 | }, 64 | "original": { 65 | "owner": "NixOS", 66 | "ref": "nixos-unstable", 67 | "repo": "nixpkgs", 68 | "type": "github" 69 | } 70 | }, 71 | "root": { 72 | "inputs": { 73 | "flake-utils": "flake-utils", 74 | "naersk": "naersk", 75 | "nixpkgs": "nixpkgs_2" 76 | } 77 | }, 78 | "systems": { 79 | "locked": { 80 | "lastModified": 1681028828, 81 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 82 | "owner": "nix-systems", 83 | "repo": "default", 84 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 85 | "type": "github" 86 | }, 87 | "original": { 88 | "owner": "nix-systems", 89 | "repo": "default", 90 | "type": "github" 91 | } 92 | } 93 | }, 94 | "root": "root", 95 | "version": 7 96 | } 97 | -------------------------------------------------------------------------------- /quickemu/core/src/args/machine/tpm.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, 3 | ffi::{OsStr, OsString}, 4 | path::{Path, PathBuf}, 5 | }; 6 | 7 | use which::which; 8 | 9 | use crate::{ 10 | arg, 11 | error::Error, 12 | oarg, 13 | utils::{ArgDisplay, EmulatorArgs, LaunchFn, LaunchFnReturn}, 14 | }; 15 | 16 | #[cfg(not(feature = "inbuilt_commands"))] 17 | use std::process::Command; 18 | 19 | impl Tpm { 20 | pub(crate) fn new(vm_dir: &Path, vm_name: &str) -> Result { 21 | #[cfg(not(feature = "inbuilt_commands"))] 22 | let binary = which("swtpm")?; 23 | 24 | let socket = vm_dir.join(format!("{vm_name}.swtpm-sock")); 25 | 26 | let mut ctrl = OsString::from("type=unixio,path="); 27 | ctrl.push(&socket); 28 | 29 | let mut tpmstate = OsString::from("dir="); 30 | tpmstate.push(vm_dir); 31 | 32 | Ok(Tpm { 33 | #[cfg(not(feature = "inbuilt_commands"))] 34 | binary, 35 | ctrl, 36 | tpmstate, 37 | socket, 38 | }) 39 | } 40 | } 41 | 42 | pub(crate) struct Tpm { 43 | #[cfg(not(feature = "inbuilt_commands"))] 44 | binary: PathBuf, 45 | ctrl: OsString, 46 | tpmstate: OsString, 47 | socket: PathBuf, 48 | } 49 | 50 | impl EmulatorArgs for Tpm { 51 | fn launch_fns(self) -> impl IntoIterator { 52 | let tpm_launch = move || { 53 | let tpm_args = [ 54 | OsStr::new("socket"), 55 | OsStr::new("--ctrl"), 56 | &self.ctrl, 57 | OsStr::new("--terminate"), 58 | OsStr::new("--tpmstate"), 59 | &self.tpmstate, 60 | OsStr::new("--tpm2"), 61 | ]; 62 | 63 | let args = |socket: PathBuf| { 64 | [ 65 | arg!("-chardev"), 66 | oarg!(socket), 67 | arg!("-tpmdev"), 68 | arg!("emulator,id=tpm0,chardev=chrtpm"), 69 | arg!("-device"), 70 | arg!("tpm-tis,tpmdev=tpm0"), 71 | ] 72 | .into_iter() 73 | .map(LaunchFnReturn::Arg) 74 | }; 75 | 76 | #[cfg(not(feature = "inbuilt_commands"))] 77 | { 78 | let child = Command::new(&self.binary) 79 | .args(tpm_args) 80 | .spawn() 81 | .map_err(|e| Error::Command("swtpm", e.to_string()))?; 82 | let pid = child.id(); 83 | 84 | Ok([ 85 | LaunchFnReturn::Process(child), 86 | LaunchFnReturn::Display(ArgDisplay { 87 | name: Cow::Borrowed("TPM"), 88 | value: Cow::Owned(format!("{} (pid: {})", self.socket.display(), pid)), 89 | }), 90 | ] 91 | .into_iter() 92 | .chain(args(self.socket)) 93 | .collect()) 94 | } 95 | #[cfg(feature = "inbuilt_commands")] 96 | { 97 | todo!() 98 | } 99 | }; 100 | Some(LaunchFn::Before(Box::new(tpm_launch))) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /quickemu/core/src/live_vm.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::File, 3 | io::Write, 4 | path::{Path, PathBuf}, 5 | }; 6 | 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use crate::{ 10 | data::{Monitor, Serial}, 11 | error::{Error, LiveVMError, MonitorError}, 12 | }; 13 | 14 | const LIVE_VM_FILENAME: &str = "quickemu-live.toml"; 15 | 16 | #[derive(Clone, Serialize, Deserialize, Debug)] 17 | pub struct LiveVM { 18 | pub pid: u32, 19 | pub ssh_port: Option, 20 | #[cfg(not(target_os = "macos"))] 21 | pub spice_port: Option, 22 | pub monitor: Monitor, 23 | pub serial: Serial, 24 | } 25 | 26 | impl LiveVM { 27 | pub(crate) fn find_active(vm_dir: &Path) -> Result, LiveVMError> { 28 | let expected_path = vm_dir.join(LIVE_VM_FILENAME); 29 | 30 | if !expected_path.is_file() { 31 | return Ok(None); 32 | } 33 | 34 | let data = std::fs::read_to_string(&expected_path).map_err(|e| LiveVMError::LiveVMDe(e.to_string()))?; 35 | let live_vm: Self = toml::from_str(&data).map_err(|e| LiveVMError::LiveVMDe(e.to_string()))?; 36 | 37 | if live_vm.is_active() { 38 | Ok(Some(live_vm)) 39 | } else { 40 | std::fs::remove_file(&expected_path).map_err(|e| LiveVMError::DelLiveFile(e.to_string()))?; 41 | Ok(None) 42 | } 43 | } 44 | pub fn send_monitor_cmd(&self, cmd: &str) -> Result { 45 | self.monitor.send_cmd(cmd) 46 | } 47 | fn is_active(&self) -> bool { 48 | #[cfg(unix)] 49 | { 50 | std::process::Command::new("kill") 51 | .arg("-0") 52 | .arg(self.pid.to_string()) 53 | .stdout(std::process::Stdio::null()) 54 | .output() 55 | .is_ok_and(|output| output.status.success()) 56 | } 57 | } 58 | pub fn kill(&self) -> Result<(), LiveVMError> { 59 | #[cfg(unix)] 60 | { 61 | std::process::Command::new("kill") 62 | .arg("-9") 63 | .arg(self.pid.to_string()) 64 | .output() 65 | .map_err(|e| LiveVMError::VMKill(e.to_string()))?; 66 | Ok(()) 67 | } 68 | } 69 | pub(crate) fn new(vm_dir: &Path, ssh_port: Option, #[cfg(not(target_os = "macos"))] spice_port: Option, monitor: Monitor, serial: Serial) -> (Self, PathBuf) { 70 | ( 71 | Self { 72 | pid: 0, 73 | ssh_port, 74 | #[cfg(not(target_os = "macos"))] 75 | spice_port, 76 | monitor, 77 | serial, 78 | }, 79 | vm_dir.join(LIVE_VM_FILENAME), 80 | ) 81 | } 82 | pub(crate) fn serialize(mut self, file: &Path, pid: u32) -> Result<(), Error> { 83 | self.pid = pid; 84 | if file.exists() { 85 | return Err(Error::FailedLiveVMSe(format!( 86 | "Live VM file already exists at {}", 87 | file.display() 88 | ))); 89 | } 90 | let data = toml::to_string_pretty(&self).map_err(|e| Error::FailedLiveVMSe(e.to_string()))?; 91 | File::create(file) 92 | .and_then(|mut file| file.write_all(data.as_bytes())) 93 | .map_err(|e| Error::FailedLiveVMSe(e.to_string()))?; 94 | Ok(()) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /quickemu/core/src/data/machine.rs: -------------------------------------------------------------------------------- 1 | use super::{default_if_empty, deserialize_size, is_default}; 2 | use derive_more::derive::Display; 3 | use itertools::chain; 4 | use serde::{Deserialize, Serialize}; 5 | use strum::{EnumIter, IntoEnumIterator}; 6 | 7 | #[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq)] 8 | pub struct Machine { 9 | pub cpu_threads: Option, 10 | #[serde(default, flatten, deserialize_with = "default_if_empty")] 11 | pub arch: Arch, 12 | #[serde(default, skip_serializing_if = "is_default")] 13 | pub boot: BootType, 14 | #[serde(default, skip_serializing_if = "is_default")] 15 | pub tpm: bool, 16 | #[serde(deserialize_with = "deserialize_size", default)] 17 | pub ram: Option, 18 | #[serde(default, skip_serializing_if = "is_default")] 19 | pub status_quo: bool, 20 | } 21 | 22 | #[derive(Display, Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] 23 | #[serde(tag = "arch")] 24 | pub enum Arch { 25 | #[display("x86_64 ({})", machine)] 26 | #[serde(rename = "x86_64")] 27 | X86_64 { 28 | #[serde(default, skip_serializing_if = "is_default")] 29 | machine: X86_64Machine, 30 | }, 31 | #[display("AArch64 ({})", machine)] 32 | #[serde(alias = "aarch64")] 33 | AArch64 { 34 | #[serde(default, skip_serializing_if = "is_default")] 35 | machine: AArch64Machine, 36 | }, 37 | #[display("Riscv64 ({})", machine)] 38 | #[serde(rename = "riscv64")] 39 | Riscv64 { 40 | #[serde(default, skip_serializing_if = "is_default")] 41 | machine: Riscv64Machine, 42 | }, 43 | } 44 | 45 | impl Arch { 46 | pub fn iter() -> impl Iterator { 47 | chain!( 48 | X86_64Machine::iter().map(|machine| Self::X86_64 { machine }), 49 | AArch64Machine::iter().map(|machine| Self::AArch64 { machine }), 50 | Riscv64Machine::iter().map(|machine| Self::Riscv64 { machine }) 51 | ) 52 | } 53 | } 54 | 55 | impl Default for Arch { 56 | fn default() -> Self { 57 | Self::X86_64 { machine: X86_64Machine::Standard } 58 | } 59 | } 60 | 61 | // Below enums will be used for future SBC / other specialized machine emulation 62 | #[derive(Display, Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize, EnumIter)] 63 | #[serde(rename_all = "snake_case")] 64 | pub enum X86_64Machine { 65 | #[default] 66 | Standard, 67 | } 68 | 69 | #[derive(Display, Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize, EnumIter)] 70 | #[serde(rename_all = "snake_case")] 71 | pub enum AArch64Machine { 72 | #[default] 73 | Standard, 74 | } 75 | 76 | #[derive(Display, Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize, EnumIter)] 77 | #[serde(rename_all = "snake_case")] 78 | pub enum Riscv64Machine { 79 | #[default] 80 | Standard, 81 | } 82 | 83 | #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] 84 | #[serde(tag = "type")] 85 | #[serde(rename_all = "snake_case")] 86 | pub enum BootType { 87 | #[serde(alias = "EFI", alias = "Efi")] 88 | Efi { 89 | #[serde(default)] 90 | secure_boot: bool, 91 | }, 92 | #[serde(alias = "Legacy", alias = "bios", alias = "BIOS")] 93 | Legacy, 94 | } 95 | impl Default for BootType { 96 | fn default() -> Self { 97 | Self::Efi { secure_boot: false } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /quickget/core/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, path::PathBuf, time::SystemTimeError}; 2 | 3 | use quickemu_core::data::Arch; 4 | 5 | use crate::fl; 6 | 7 | #[derive(derive_more::From, Debug)] 8 | pub enum ConfigSearchError { 9 | FailedCacheDir, 10 | InvalidCacheDir(PathBuf), 11 | #[from] 12 | InvalidSystemTime(SystemTimeError), 13 | #[from] 14 | FailedCacheFile(std::io::Error), 15 | #[from] 16 | FailedDownload(reqwest::Error), 17 | #[from] 18 | FailedJson(serde_json::Error), 19 | RequiredOS, 20 | RequiredRelease, 21 | RequiredEdition, 22 | InvalidOS(String), 23 | InvalidRelease(String, String), 24 | InvalidEdition(String), 25 | InvalidArchitecture(Arch), 26 | NoEditions, 27 | } 28 | 29 | impl std::error::Error for ConfigSearchError {} 30 | impl fmt::Display for ConfigSearchError { 31 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 32 | let text = match self { 33 | Self::FailedCacheDir => fl!("failed-cache-dir"), 34 | Self::InvalidCacheDir(dir) => fl!("invalid-cache-dir", dir = dir.display().to_string()), 35 | Self::InvalidSystemTime(err) => fl!("invalid-system-time", err = err.to_string()), 36 | Self::FailedCacheFile(err) => fl!("failed-cache-file", err = err.to_string()), 37 | Self::FailedDownload(err) => fl!("failed-download", err = err.to_string()), 38 | Self::FailedJson(err) => fl!("failed-json", err = err.to_string()), 39 | Self::RequiredOS => fl!("required-os"), 40 | Self::RequiredRelease => fl!("required-release"), 41 | Self::RequiredEdition => fl!("required-edition"), 42 | Self::InvalidOS(os) => fl!("invalid-os", os = os), 43 | Self::InvalidRelease(rel, os) => fl!("invalid-release", rel = rel, os = os), 44 | Self::InvalidEdition(edition) => fl!("invalid-edition", edition = edition), 45 | Self::InvalidArchitecture(arch) => fl!("invalid-arch", arch = arch.to_string()), 46 | Self::NoEditions => fl!("no-editions"), 47 | }; 48 | f.write_str(&text) 49 | } 50 | } 51 | 52 | #[derive(derive_more::From, Debug)] 53 | pub enum DLError { 54 | UnsupportedSource(String), 55 | InvalidVMName(String), 56 | #[from] 57 | ConfigFileError(std::io::Error), 58 | #[from] 59 | ConfigDataError(toml::ser::Error), 60 | DownloadError(PathBuf), 61 | InvalidChecksum(String), 62 | FailedValidation(String, String), 63 | DirAlreadyExists(PathBuf), 64 | } 65 | 66 | impl std::error::Error for DLError {} 67 | impl fmt::Display for DLError { 68 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 69 | let text = match self { 70 | Self::UnsupportedSource(os) => fl!("unsupported-source", os = os), 71 | Self::InvalidVMName(vm_name) => fl!("invalid-vm-name", vm_name = vm_name), 72 | Self::ConfigFileError(err) => fl!("config-file-error", err = err.to_string()), 73 | Self::ConfigDataError(err) => fl!("config-data-error", err = err.to_string()), 74 | Self::DownloadError(file) => fl!("download-error", file = file.display().to_string()), 75 | Self::InvalidChecksum(cs) => fl!("invalid-checksum", cs = cs), 76 | Self::FailedValidation(expected, actual) => fl!("failed-validation", expected = expected, actual = actual), 77 | Self::DirAlreadyExists(dir) => fl!("dir-exists", dir = dir.display().to_string()), 78 | }; 79 | f.write_str(&text) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /quickget/cli/src/main.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | mod i18n; 3 | 4 | use anyhow::Result; 5 | use clap::Parser; 6 | use config::ListType; 7 | use quickemu_core::data::Arch; 8 | use quickget_core::{QuickgetConfig, QuickgetInstance}; 9 | use std::io::Write; 10 | 11 | #[tokio::main] 12 | async fn main() -> Result<()> { 13 | let args = Args::parse(); 14 | env_logger::builder().filter_level(args.verbose.log_level_filter()).init(); 15 | 16 | let arch = args 17 | .arch 18 | .map(|a| { 19 | Ok(match a.as_str() { 20 | "x86_64" => Arch::X86_64 { machine: Default::default() }, 21 | "aarch64" | "AArch64" => Arch::AArch64 { machine: Default::default() }, 22 | "riscv64" => Arch::Riscv64 { machine: Default::default() }, 23 | _ => fl_bail!("invalid-architecture", architecture = a), 24 | }) 25 | }) 26 | .transpose()?; 27 | 28 | if let Some(list_type) = args.list { 29 | fl_ensure!(args.other.is_empty(), "list-specified-os"); 30 | return config::list(list_type, args.refresh).await; 31 | } 32 | 33 | let config = config::get(&args.other, arch.as_ref(), args.refresh).await?; 34 | println!("{config:#?}"); 35 | let file = create_config(config).await?; 36 | println!("Completed. File {file:?}"); 37 | 38 | Ok(()) 39 | } 40 | async fn create_config(config: QuickgetConfig) -> Result { 41 | let mut instance = QuickgetInstance::new(config, std::env::current_dir().unwrap())?; 42 | instance.create_vm_dir(true)?; 43 | let downloads = instance.get_downloads(); 44 | let docker_commands = instance.get_docker_commands(); 45 | for mut command in docker_commands { 46 | let status = command.status()?; 47 | fl_ensure!(status.success(), "docker-command-failed", command = format!("{:?}", command)); 48 | } 49 | 50 | let client = reqwest::Client::new(); 51 | for download in downloads { 52 | let mut request = client.get(download.url); 53 | 54 | if let Some(headers) = download.headers { 55 | request = request.headers(headers); 56 | } 57 | let mut response = request.send().await?; 58 | let length = response.content_length().unwrap_or_default(); 59 | 60 | let progress = indicatif::ProgressBar::new(length); 61 | progress.set_style( 62 | indicatif::ProgressStyle::with_template("{bar:30.blue/red} ({percent}%) {bytes:>12.green} / {total_bytes:<12.green} {bytes_per_sec:>13.blue} - ETA: {eta_precise}") 63 | .unwrap() 64 | .progress_chars("━╾╴─"), 65 | ); 66 | 67 | let mut file = std::fs::File::create(download.path)?; 68 | while let Some(chunk) = response.chunk().await? { 69 | progress.inc(chunk.len() as u64); 70 | file.write_all(&chunk)?; 71 | } 72 | } 73 | 74 | instance.create_config().map_err(Into::into) 75 | } 76 | 77 | #[derive(Debug, Parser)] 78 | #[clap(group = clap::ArgGroup::new("actions").multiple(false))] 79 | struct Args { 80 | #[clap(short, long)] 81 | arch: Option, 82 | #[command(flatten)] 83 | verbose: clap_verbosity_flag::Verbosity, 84 | #[clap(short, long)] 85 | refresh: bool, 86 | #[clap(short, long, group = "actions")] 87 | open_homepage: bool, 88 | #[clap(short, long, group = "actions")] 89 | url: bool, 90 | #[clap(short, long, group = "actions")] 91 | download_only: bool, 92 | #[clap(short, long, group = "actions")] 93 | list: Option>, 94 | other: Vec, 95 | } 96 | -------------------------------------------------------------------------------- /quickemu/core/src/args/images.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, collections::HashSet, path::Path, thread, time::Duration}; 2 | 3 | use disks::DiskArgs; 4 | use img::ImgArgs; 5 | use iso::IsoArgs; 6 | use itertools::chain; 7 | 8 | use crate::{ 9 | data::{GuestOS, Images, Monitor}, 10 | error::{Error, Warning}, 11 | utils::{ArgDisplay, EmulatorArgs, LaunchFn, LaunchFnReturn, QemuArg}, 12 | }; 13 | 14 | mod disks; 15 | mod img; 16 | mod iso; 17 | 18 | impl Images { 19 | pub(crate) fn args(&self, guest: GuestOS, vm_dir: &Path, status_quo: bool, monitor: Monitor) -> Result<(ImageArgs, Option), Error> { 20 | let mut used_indices = HashSet::new(); 21 | let disks = self.disk_args(guest, vm_dir, status_quo, &mut used_indices)?; 22 | let isos = self.iso_args(disks.installed(), guest, vm_dir, &mut used_indices)?; 23 | let imgs = self.img_args(disks.installed(), vm_dir, guest)?; 24 | 25 | let monitor_cmds = matches!(guest, GuestOS::Windows).then(|| MonitorCmds { 26 | monitor, 27 | cmds: (0..5) 28 | .map(|_| MonitorCmd { 29 | wait_before: Duration::from_secs(1), 30 | command: Cow::Borrowed("sendkey ret"), 31 | }) 32 | .collect(), 33 | }); 34 | 35 | Ok((ImageArgs { disks, isos, imgs, monitor_cmds }, None)) 36 | } 37 | } 38 | 39 | pub(crate) struct ImageArgs<'a> { 40 | disks: DiskArgs<'a>, 41 | isos: IsoArgs<'a>, 42 | imgs: ImgArgs<'a>, 43 | monitor_cmds: Option, 44 | } 45 | 46 | impl EmulatorArgs for ImageArgs<'_> { 47 | fn display(&self) -> impl IntoIterator { 48 | chain!( 49 | self.disks.display(), 50 | self.isos.display(), 51 | self.imgs.display(), 52 | self.monitor_cmds.as_ref().map(|cmds| cmds.display()).into_iter().flatten(), 53 | ) 54 | } 55 | fn qemu_args(&self) -> impl IntoIterator { 56 | chain!( 57 | self.disks.qemu_args(), 58 | self.isos.qemu_args(), 59 | self.imgs.qemu_args(), 60 | self.monitor_cmds.as_ref().map(|cmds| cmds.qemu_args()).into_iter().flatten(), 61 | ) 62 | } 63 | fn launch_fns(self) -> impl IntoIterator { 64 | chain!( 65 | self.disks.launch_fns(), 66 | self.isos.launch_fns(), 67 | self.imgs.launch_fns(), 68 | self.monitor_cmds.map(|cmds| cmds.launch_fns()).into_iter().flatten() 69 | ) 70 | } 71 | } 72 | 73 | struct MonitorCmds { 74 | monitor: Monitor, 75 | cmds: Vec, 76 | } 77 | 78 | struct MonitorCmd { 79 | wait_before: Duration, 80 | command: Cow<'static, str>, 81 | } 82 | 83 | impl EmulatorArgs for MonitorCmds { 84 | fn launch_fns(self) -> impl IntoIterator { 85 | let launch_fn = Box::new(move || { 86 | let thread = thread::spawn(move || { 87 | for cmd in self.cmds { 88 | thread::sleep(cmd.wait_before); 89 | log::info!("Sending monitor command: {}", cmd.command); 90 | self.monitor 91 | .send_cmd(&cmd.command) 92 | .map_err(|e| Error::MonitorCommand(e.to_string()))?; 93 | } 94 | Ok(()) 95 | }); 96 | 97 | Ok(vec![LaunchFnReturn::Thread(thread)]) 98 | }); 99 | 100 | Some(LaunchFn::After(launch_fn)) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /quickemu/core/src/args/images/iso.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, collections::HashSet, ffi::OsString, path::Path}; 2 | 3 | use crate::{ 4 | arg, 5 | data::{GuestOS, Images}, 6 | error::Error, 7 | oarg, 8 | utils::{ArgDisplay, EmulatorArgs, QemuArg}, 9 | }; 10 | 11 | impl<'a> Images { 12 | pub(crate) fn iso_args(&'a self, installed: bool, guest: GuestOS, vm_dir: &Path, used_indices: &mut HashSet) -> Result, Error> { 13 | let mut key = 0; 14 | let mut images = Vec::new(); 15 | 16 | if !installed && matches!(guest, GuestOS::Windows) { 17 | let unattended: Cow<'a, Path> = Cow::Owned(vm_dir.join("unattended.iso")); 18 | if unattended.exists() { 19 | images.push(MountedIso::new(unattended, &mut 2, used_indices)); 20 | } 21 | } 22 | 23 | let images = self 24 | .iso 25 | .iter() 26 | .filter(|iso| iso.always_mount || !installed) 27 | .map(|iso| { 28 | if iso.path.is_absolute() { 29 | Cow::Borrowed(iso.path.as_path()) 30 | } else { 31 | Cow::Owned(vm_dir.join(&iso.path)) 32 | } 33 | }) 34 | .try_fold(images, |mut acc, path| { 35 | if !path.exists() { 36 | return Err(Error::NonexistentImage(path.display().to_string())); 37 | } 38 | acc.push(MountedIso::new(path, &mut key, used_indices)); 39 | Ok(acc) 40 | })?; 41 | 42 | Ok(IsoArgs { images, guest }) 43 | } 44 | } 45 | 46 | pub(crate) struct IsoArgs<'a> { 47 | images: Vec>, 48 | guest: GuestOS, 49 | } 50 | 51 | impl EmulatorArgs for IsoArgs<'_> { 52 | fn display(&self) -> impl IntoIterator { 53 | self.images.iter().map(|iso| ArgDisplay { 54 | name: Cow::Borrowed("ISO"), 55 | value: Cow::Owned(iso.path.display().to_string()), 56 | }) 57 | } 58 | fn qemu_args(&self) -> impl IntoIterator { 59 | let mut args = Vec::new(); 60 | match self.guest { 61 | GuestOS::FreeDOS => args.extend([arg!("-boot"), arg!("order=dc")]), 62 | GuestOS::ReactOS => args.extend([arg!("-boot"), arg!("order=d")]), 63 | _ => {} 64 | } 65 | args.into_iter().chain(self.images.iter().flat_map(|iso| iso.args(self.guest))) 66 | } 67 | } 68 | 69 | struct MountedIso<'a> { 70 | path: Cow<'a, Path>, 71 | index: u32, 72 | } 73 | 74 | impl<'a> MountedIso<'a> { 75 | fn new(path: Cow<'a, Path>, key: &mut u32, used_indices: &mut HashSet) -> Self { 76 | while !used_indices.insert(*key) { 77 | *key += 1; 78 | } 79 | let mounted_iso = Self { path, index: *key }; 80 | *key += 1; 81 | mounted_iso 82 | } 83 | 84 | fn args(&self, guest: GuestOS) -> [QemuArg; 2] { 85 | let arg = match guest { 86 | GuestOS::ReactOS => self.reactos_arg(), 87 | _ => { 88 | let mut arg = OsString::from("media=cdrom,index="); 89 | arg.push(self.index.to_string()); 90 | arg.push(",file="); 91 | arg.push(self.path.as_ref()); 92 | oarg!(arg) 93 | } 94 | }; 95 | [arg!("-drive"), arg] 96 | } 97 | 98 | fn reactos_arg(&self) -> QemuArg { 99 | let mut arg = OsString::from("if=ide,index="); 100 | arg.push(self.index.to_string()); 101 | arg.push(",media=cdrom,file="); 102 | arg.push(self.path.as_ref()); 103 | oarg!(arg) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /quickemu/core/src/data/image.rs: -------------------------------------------------------------------------------- 1 | use super::{default_if_empty, deserialize_size, is_default}; 2 | use serde::{Deserialize, Serialize}; 3 | use std::path::PathBuf; 4 | 5 | #[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] 6 | pub struct Images { 7 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 8 | pub disk: Vec, 9 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 10 | pub iso: Vec, 11 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 12 | pub img: Vec, 13 | } 14 | 15 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 16 | pub struct Image { 17 | #[serde(default, skip_serializing_if = "is_default")] 18 | pub path: PathBuf, 19 | #[serde(default, skip_serializing_if = "is_default")] 20 | pub always_mount: bool, 21 | } 22 | 23 | #[derive(PartialEq, Clone, Default, Debug, Serialize, Deserialize)] 24 | pub struct DiskImage { 25 | pub path: PathBuf, 26 | #[serde(deserialize_with = "deserialize_size", default)] 27 | pub size: Option, 28 | #[serde(default, flatten, skip_serializing_if = "is_default")] 29 | #[serde(deserialize_with = "default_if_empty")] 30 | pub format: DiskFormat, 31 | } 32 | 33 | #[derive(Clone, Copy, Default, Debug, PartialEq, Serialize, Deserialize)] 34 | #[serde(rename_all = "snake_case")] 35 | pub enum PreAlloc { 36 | #[default] 37 | Off, 38 | Metadata, 39 | Falloc, 40 | Full, 41 | } 42 | 43 | #[derive(Copy, Serialize, Deserialize, Clone, Debug, PartialEq)] 44 | #[serde(tag = "format")] 45 | #[serde(rename_all = "snake_case")] 46 | pub enum DiskFormat { 47 | Qcow2 { 48 | #[serde(default, skip_serializing_if = "is_default")] 49 | preallocation: PreAlloc, 50 | }, 51 | Raw { 52 | #[serde(default, skip_serializing_if = "is_default")] 53 | preallocation: PreAlloc, 54 | }, 55 | Qed, 56 | Qcow, 57 | Vdi, 58 | Vpc, 59 | Vhdx, 60 | } 61 | 62 | impl Default for DiskFormat { 63 | fn default() -> Self { 64 | Self::Qcow2 { preallocation: PreAlloc::Off } 65 | } 66 | } 67 | #[cfg(feature = "quickemu")] 68 | impl DiskFormat { 69 | pub(crate) fn prealloc_enabled(&self) -> bool { 70 | match self { 71 | Self::Qcow2 { preallocation } => !matches!(preallocation, PreAlloc::Off), 72 | Self::Raw { preallocation } => !matches!(preallocation, PreAlloc::Off), 73 | _ => false, 74 | } 75 | } 76 | pub(crate) fn prealloc_arg(&self) -> &str { 77 | match self { 78 | Self::Qcow2 { preallocation } => match preallocation { 79 | PreAlloc::Off => "lazy_refcounts=on,preallocation=off,nocow=on", 80 | PreAlloc::Metadata => "lazy_refcounts=on,preallocation=metadata,nocow=on", 81 | PreAlloc::Falloc => "lazy_refcounts=on,preallocation=falloc,nocow=on", 82 | PreAlloc::Full => "lazy_refcounts=on,preallocation=full,nocow=on", 83 | }, 84 | Self::Raw { preallocation } => match preallocation { 85 | PreAlloc::Off => "preallocation=off", 86 | PreAlloc::Metadata => "preallocation=metadata", 87 | PreAlloc::Falloc => "preallocation=falloc", 88 | PreAlloc::Full => "preallocation=full", 89 | }, 90 | _ => "preallocation=off", 91 | } 92 | } 93 | } 94 | impl AsRef for DiskFormat { 95 | fn as_ref(&self) -> &str { 96 | match self { 97 | Self::Qcow2 { .. } => "qcow2", 98 | Self::Raw { .. } => "raw", 99 | Self::Qed => "qed", 100 | Self::Qcow => "qcow", 101 | Self::Vdi => "vdi", 102 | Self::Vpc => "vpc", 103 | Self::Vhdx => "vhdx", 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /quickemu/core/src/args/io/usb.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | arg, 3 | data::{GuestOS, MacOSRelease, USBController}, 4 | utils::{EmulatorArgs, QemuArg}, 5 | }; 6 | 7 | impl GuestOS { 8 | pub(crate) fn default_usb_controller(&self) -> USBController { 9 | match self { 10 | GuestOS::Solaris => USBController::Xhci, 11 | GuestOS::MacOS { release } if release >= &MacOSRelease::BigSur => USBController::Xhci, 12 | _ => USBController::Ehci, 13 | } 14 | } 15 | } 16 | 17 | impl USBController { 18 | pub(crate) fn usb_args(&self, guest: GuestOS) -> USBArgs { 19 | #[cfg(not(target_os = "macos"))] 20 | let passthrough_controller = match self { 21 | Self::Ehci => Some(PassthroughController::UsbEhci), 22 | Self::Xhci => match guest { 23 | GuestOS::MacOS { release } if release >= MacOSRelease::BigSur => Some(PassthroughController::NecUsbXhci), 24 | _ => Some(PassthroughController::QemuXhci), 25 | }, 26 | Self::None => None, 27 | }; 28 | USBArgs { 29 | controller: *self, 30 | #[cfg(not(target_os = "macos"))] 31 | passthrough_controller, 32 | } 33 | } 34 | } 35 | 36 | pub(crate) struct USBArgs { 37 | controller: USBController, 38 | #[cfg(not(target_os = "macos"))] 39 | passthrough_controller: Option, 40 | } 41 | 42 | #[cfg(not(target_os = "macos"))] 43 | enum PassthroughController { 44 | NecUsbXhci, 45 | UsbEhci, 46 | QemuXhci, 47 | } 48 | 49 | #[cfg(not(target_os = "macos"))] 50 | impl PassthroughController { 51 | fn spice_arg(&self) -> &'static str { 52 | match self { 53 | Self::NecUsbXhci => "nec-usb-xhci,id=spicepass", 54 | Self::UsbEhci => "usb-ehci,id=spicepass", 55 | Self::QemuXhci => "qemu-xhci,id=spicepass", 56 | } 57 | } 58 | } 59 | 60 | impl EmulatorArgs for USBArgs { 61 | fn qemu_args(&self) -> impl IntoIterator { 62 | let mut args = vec![arg!("-device"), arg!("virtio-rng-pci"), arg!("-object"), arg!("rng-random,id=rng0,filename=/dev/urandom")]; 63 | 64 | #[cfg(not(target_os = "macos"))] 65 | if let Some(passthrough_controller) = &self.passthrough_controller { 66 | args.extend([ 67 | arg!("-device"), 68 | arg!(passthrough_controller.spice_arg()), 69 | arg!("-chardev"), 70 | arg!("spicevmc,id=usbredirchardev1,name=usbredir"), 71 | arg!("-device"), 72 | arg!("usb-redir,chardev=usbredirchardev1,id=usbredirdev1"), 73 | arg!("-chardev"), 74 | arg!("spicevmc,id=usbredirchardev2,name=usbredir"), 75 | arg!("-device"), 76 | arg!("usb-redir,chardev=usbredirchardev2,id=usbredirdev2"), 77 | arg!("-chardev"), 78 | arg!("spicevmc,id=usbredirchardev3,name=usbredir"), 79 | arg!("-device"), 80 | arg!("usb-redir,chardev=usbredirchardev3,id=usbredirdev3"), 81 | arg!("-device"), 82 | arg!("pci-ohci,id=smartpass"), 83 | arg!("-device"), 84 | arg!("usb-ccid"), 85 | ]); 86 | } 87 | 88 | match self.controller { 89 | USBController::Ehci => args.extend([arg!("-device"), arg!("usb-ehci,id=input")]), 90 | USBController::Xhci => args.extend([arg!("-device"), arg!("qemu-xhci,id=input")]), 91 | _ => (), 92 | } 93 | 94 | #[cfg(feature = "smartcard_args")] 95 | args.extend([arg!("-chardev"), arg!("spicevmc,id=ccid,name=smartcard"), arg!("-device"), arg!("ccid-card-passthru,chardev=ccid")]); 96 | 97 | args 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /quickemu/core/src/args/network/monitor.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, 3 | ffi::OsString, 4 | io::{Read, Write}, 5 | net::TcpStream, 6 | }; 7 | 8 | use crate::{ 9 | arg, 10 | data::{Monitor, MonitorArg, MonitorInner}, 11 | error::{Error, MonitorError}, 12 | utils::{find_port, ArgDisplay, EmulatorArgs, QemuArg}, 13 | }; 14 | 15 | #[cfg(unix)] 16 | use std::os::unix::net::UnixStream; 17 | 18 | impl Monitor { 19 | pub fn send_cmd(&self, command: &str) -> Result { 20 | let mut response = String::new(); 21 | match self { 22 | MonitorInner::Telnet { address } => { 23 | let mut stream = TcpStream::connect(address.as_ref()).map_err(MonitorError::Read)?; 24 | stream.write_all(command.as_bytes()).map_err(MonitorError::Write)?; 25 | stream.write_all(b"\r\n").map_err(MonitorError::Write)?; 26 | stream.shutdown(std::net::Shutdown::Write).map_err(MonitorError::Write)?; 27 | stream 28 | .set_read_timeout(Some(std::time::Duration::from_secs(1))) 29 | .map_err(MonitorError::Read)?; 30 | stream.read_to_string(&mut response).map_err(MonitorError::Read)?; 31 | } 32 | #[cfg(unix)] 33 | MonitorInner::Socket { socketpath: Some(ref socketpath) } => { 34 | let mut stream = UnixStream::connect(socketpath).map_err(MonitorError::Read)?; 35 | stream.write_all(command.as_bytes()).map_err(MonitorError::Write)?; 36 | stream.write_all(b"\r\n").map_err(MonitorError::Write)?; 37 | stream.shutdown(std::net::Shutdown::Write).map_err(MonitorError::Write)?; 38 | stream 39 | .set_read_timeout(Some(std::time::Duration::from_secs(1))) 40 | .map_err(MonitorError::Read)?; 41 | stream.read_to_string(&mut response).map_err(MonitorError::Read)?; 42 | } 43 | _ => return Err(MonitorError::NoMonitor), 44 | } 45 | Ok(response) 46 | } 47 | } 48 | 49 | impl MonitorInner { 50 | pub(crate) fn validate(&mut self) -> Result<(), Error> { 51 | if let Self::Telnet { address } = self { 52 | let defined_port = address.as_ref().port(); 53 | let port = find_port(defined_port, 9).ok_or(Error::UnavailablePort(defined_port))?; 54 | address.as_mut().set_port(port); 55 | } 56 | Ok(()) 57 | } 58 | } 59 | 60 | impl EmulatorArgs for MonitorInner { 61 | fn display(&self) -> impl IntoIterator { 62 | let value = match self { 63 | Self::None => Cow::Borrowed("None"), 64 | Self::Telnet { address } => Cow::Owned(format!("telnet {}", address.as_ref())), 65 | #[cfg(unix)] 66 | Self::Socket { socketpath } => Cow::Owned(format!( 67 | "socket {}", 68 | socketpath.as_ref().expect("Socketpath should be filled").display() 69 | )), 70 | }; 71 | Some(ArgDisplay { 72 | name: Cow::Borrowed(T::display()), 73 | value, 74 | }) 75 | } 76 | fn qemu_args(&self) -> impl IntoIterator { 77 | let arg = match self { 78 | Self::None => arg!("none"), 79 | Self::Telnet { address } => { 80 | let mut telnet = OsString::from("telnet:"); 81 | telnet.push(address.as_ref().to_string()); 82 | telnet.push(",server,nowait"); 83 | Cow::Owned(telnet) 84 | } 85 | #[cfg(unix)] 86 | Self::Socket { socketpath } => { 87 | let mut socket = OsString::from("unix:"); 88 | socket.push(socketpath.as_ref().expect("Socketpath should be filled")); 89 | socket.push(",server,nowait"); 90 | Cow::Owned(socket) 91 | } 92 | }; 93 | 94 | [arg!(T::arg()), arg] 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /quickemu/core/src/args/guest.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | arg, 3 | data::{Arch, GuestOS}, 4 | error::{Error, Warning}, 5 | oarg, 6 | utils::{EmulatorArgs, QemuArg}, 7 | }; 8 | 9 | impl GuestOS { 10 | #[cfg(target_arch = "x86_64")] 11 | pub(crate) fn validate_cpu(&self) -> Result<(), Error> { 12 | use crate::data::MacOSRelease; 13 | 14 | let cpuid = raw_cpuid::CpuId::new(); 15 | log::trace!("Testing architecture. Found CPUID: {cpuid:?}"); 16 | 17 | let Some(cpu_features) = cpuid.get_feature_info() else { return Ok(()) }; 18 | 19 | if let GuestOS::MacOS { release } = self { 20 | if !cpu_features.has_sse41() { 21 | return Err(Error::Instructions("SSE4.1")); 22 | } 23 | if release >= &MacOSRelease::Ventura { 24 | let Some(extended_features) = cpuid.get_extended_feature_info() else { return Ok(()) }; 25 | 26 | if !cpu_features.has_sse42() { 27 | return Err(Error::Instructions("SSE4.2")); 28 | } 29 | if !extended_features.has_avx2() { 30 | return Err(Error::Instructions("AVX2")); 31 | } 32 | } 33 | } 34 | 35 | Ok(()) 36 | } 37 | pub(crate) fn tweaks(&self, arch: Arch) -> Result<(GuestTweaks, Vec), Error> { 38 | let mut warnings = Vec::new(); 39 | 40 | #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] 41 | let hw_virt = arch.enable_hw_virt().unwrap_or_else(|w| { 42 | warnings.push(w); 43 | false 44 | }); 45 | #[cfg(target_os = "linux")] 46 | let discard_lost_ticks = hw_virt && matches!(self, Self::Windows | Self::WindowsServer | Self::MacOS { .. }); 47 | let disable_s3 = matches!(self, Self::Windows | Self::WindowsServer | Self::MacOS { .. }); 48 | let osk = matches!(self, Self::MacOS { .. }); 49 | 50 | Ok(( 51 | GuestTweaks { 52 | #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] 53 | hw_virt, 54 | #[cfg(target_os = "linux")] 55 | discard_lost_ticks, 56 | disable_s3, 57 | osk, 58 | }, 59 | warnings, 60 | )) 61 | } 62 | } 63 | 64 | const OSK: &[u8] = &[ 65 | 0x6f, 0x75, 0x72, 0x68, 0x61, 0x72, 0x64, 0x77, 0x6f, 0x72, 0x6b, 0x62, 0x79, 0x74, 0x68, 0x65, 0x73, 0x65, 0x77, 0x6f, 0x72, 0x64, 0x73, 0x67, 0x75, 0x61, 0x72, 0x64, 0x65, 0x64, 0x70, 0x6c, 66 | 0x65, 0x61, 0x73, 0x65, 0x64, 0x6f, 0x6e, 0x74, 0x73, 0x74, 0x65, 0x61, 0x6c, 0x28, 0x63, 0x29, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x63, 67 | ]; 68 | 69 | pub(crate) struct GuestTweaks { 70 | #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] 71 | pub hw_virt: bool, 72 | #[cfg(target_os = "linux")] 73 | discard_lost_ticks: bool, 74 | disable_s3: bool, 75 | osk: bool, 76 | } 77 | 78 | impl EmulatorArgs for GuestTweaks { 79 | fn qemu_args(&self) -> impl IntoIterator { 80 | let mut tweaks = Vec::new(); 81 | 82 | #[cfg(target_os = "linux")] 83 | if self.hw_virt { 84 | tweaks.extend([arg!("-accel"), arg!("kvm")]); 85 | } 86 | #[cfg(target_os = "macos")] 87 | if self.hw_virt { 88 | tweaks.extend([arg!("-accel"), arg!("hvf")]); 89 | } 90 | #[cfg(target_os = "windows")] 91 | if self.hw_virt { 92 | tweaks.extend([arg!("-accel"), arg!("whpx")]); 93 | } 94 | 95 | #[cfg(target_os = "linux")] 96 | if self.discard_lost_ticks { 97 | tweaks.extend([arg!("-global"), arg!("kvm-pit.lost_tick_policy=discard")]); 98 | } 99 | 100 | if self.disable_s3 { 101 | tweaks.extend([arg!("-global"), arg!("ICH9-LPC.disable_s3=1")]); 102 | } 103 | 104 | if self.osk { 105 | let osk = format!("isa-applesmc,osk={}", std::str::from_utf8(OSK).unwrap()); 106 | tweaks.extend([arg!("-device"), oarg!(osk)]); 107 | } 108 | 109 | tweaks 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /quickemu/core/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, 3 | ffi::OsStr, 4 | fmt, 5 | net::{Ipv4Addr, SocketAddrV4, TcpListener}, 6 | thread::JoinHandle, 7 | }; 8 | 9 | #[cfg(feature = "inbuilt_commands")] 10 | use memfd_exec::Child; 11 | #[cfg(not(feature = "inbuilt_commands"))] 12 | use std::process::Child; 13 | 14 | use crate::error::Error; 15 | 16 | #[derive(Debug)] 17 | pub struct ArgDisplay { 18 | // e.g. "CPU" 19 | pub name: Cow<'static, str>, 20 | // e.g. "1 socket (Ryzen 5 5600), 2 cores, 4 threads" 21 | pub value: Cow<'static, str>, 22 | } 23 | pub type QemuArg = Cow<'static, OsStr>; 24 | 25 | pub trait EmulatorArgs: Sized { 26 | fn display(&self) -> impl IntoIterator { 27 | std::iter::empty() 28 | } 29 | fn qemu_args(&self) -> impl IntoIterator { 30 | std::iter::empty() 31 | } 32 | fn launch_fns(self) -> impl IntoIterator { 33 | std::iter::empty() 34 | } 35 | } 36 | 37 | type LaunchFnReturnType = Result, Error>; 38 | pub type LaunchFnInner = Box LaunchFnReturnType>; 39 | 40 | pub enum LaunchFn { 41 | Before(LaunchFnInner), 42 | After(LaunchFnInner), 43 | } 44 | 45 | impl LaunchFn { 46 | pub fn call(self) -> LaunchFnReturnType { 47 | match self { 48 | LaunchFn::Before(inner) => inner(), 49 | LaunchFn::After(inner) => inner(), 50 | } 51 | } 52 | } 53 | 54 | impl std::fmt::Debug for LaunchFn { 55 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 56 | match self { 57 | LaunchFn::Before(_) => write!(f, "LaunchFn::Before(_)"), 58 | LaunchFn::After(_) => write!(f, "LaunchFn::After(_)"), 59 | } 60 | } 61 | } 62 | 63 | #[derive(Debug)] 64 | pub enum LaunchFnReturn { 65 | Thread(JoinHandle>), 66 | Process(Child), 67 | Arg(QemuArg), 68 | Display(ArgDisplay), 69 | } 70 | 71 | pub(crate) fn plural_if(b: bool) -> &'static str { 72 | if b { 73 | "s" 74 | } else { 75 | "" 76 | } 77 | } 78 | 79 | pub(crate) fn find_port(port: u16, offset: u16) -> Option { 80 | (port..=port + offset).find(|port| { 81 | let port = SocketAddrV4::new(Ipv4Addr::LOCALHOST, *port); 82 | TcpListener::bind(port).is_ok() 83 | }) 84 | } 85 | 86 | #[macro_export] 87 | macro_rules! oarg { 88 | ($arg:expr) => { 89 | ::std::borrow::Cow::Owned(::std::ffi::OsString::from($arg)) 90 | }; 91 | } 92 | 93 | #[macro_export] 94 | macro_rules! arg { 95 | ($arg:expr) => { 96 | ::std::borrow::Cow::Borrowed(::std::ffi::OsStr::new($arg)) 97 | }; 98 | } 99 | 100 | #[macro_export] 101 | macro_rules! qemu_args { 102 | ($($arg:expr),* $(,)?) => { 103 | { 104 | let mut warnings: Vec = Vec::new(); 105 | let mut qemu_args: Vec = Vec::new(); 106 | $( 107 | { 108 | let (args, warn) = $arg?; 109 | warnings.extend(warn); 110 | qemu_args.extend(args.qemu_args()); 111 | } 112 | )* 113 | Ok::<_, Error>((qemu_args, warnings)) 114 | } 115 | }; 116 | } 117 | 118 | #[macro_export] 119 | macro_rules! full_qemu_args { 120 | ($($arg:expr),* $(,)?) => { 121 | { 122 | let mut warnings: Vec = Vec::new(); 123 | let mut display: Vec = Vec::new(); 124 | let mut qemu_args: Vec = Vec::new(); 125 | let mut before_launch_fns = Vec::new(); 126 | let mut after_launch_fns = Vec::new(); 127 | $( 128 | { 129 | let (args, warn) = $arg?; 130 | warnings.extend(warn); 131 | display.extend(args.display()); 132 | qemu_args.extend(args.qemu_args()); 133 | 134 | for launch_fn in args.launch_fns() { 135 | match launch_fn { 136 | LaunchFn::Before(_) => before_launch_fns.push(launch_fn), 137 | LaunchFn::After(_) => after_launch_fns.push(launch_fn), 138 | } 139 | }; 140 | } 141 | )* 142 | 143 | Ok::<_, Error>(QemuArgs { 144 | qemu_args, 145 | warnings, 146 | display, 147 | before_launch_fns, 148 | after_launch_fns, 149 | }) 150 | } 151 | }; 152 | } 153 | -------------------------------------------------------------------------------- /quickemu/core/src/args/io.rs: -------------------------------------------------------------------------------- 1 | use audio::Audio; 2 | use display::DisplayArgs; 3 | use itertools::chain; 4 | use public_dir::PublicDirArgs; 5 | use usb::USBArgs; 6 | 7 | use crate::{ 8 | data::{Arch, GuestOS, Io, Keyboard, KeyboardLayout, Mouse}, 9 | error::{Error, Warning}, 10 | utils::{ArgDisplay, EmulatorArgs, LaunchFn, QemuArg}, 11 | }; 12 | 13 | mod audio; 14 | mod display; 15 | mod keyboard; 16 | mod mouse; 17 | mod public_dir; 18 | mod usb; 19 | 20 | #[cfg(not(target_os = "macos"))] 21 | mod spice; 22 | #[cfg(not(target_os = "macos"))] 23 | use crate::data::DisplayType; 24 | 25 | impl<'a> Io { 26 | pub fn args(&'a self, arch: Arch, guest: GuestOS, vm_name: &'a str) -> Result<(IoArgs<'a>, Vec), Error> { 27 | let mut warnings = Vec::new(); 28 | 29 | let keyboard = self.keyboard.unwrap_or(guest.default_keyboard()); 30 | let usb_controller = self.usb_controller.unwrap_or(guest.default_usb_controller()); 31 | 32 | let soundcard = self.soundcard.unwrap_or(guest.default_soundcard()); 33 | soundcard.validate(usb_controller)?; 34 | let (audio, audio_warnings) = self.display.audio(soundcard)?; 35 | warnings.extend(audio_warnings); 36 | 37 | let display = self.display.args(guest, arch)?; 38 | 39 | let public_dir_args = self.public_dir.as_ref().as_deref().map(|d| PublicDirArgs::new(d, guest)); 40 | 41 | #[cfg(not(target_os = "macos"))] 42 | let spice = matches!(self.display.display_type, DisplayType::Spice { .. } | DisplayType::SpiceApp) 43 | .then(|| { 44 | let public_dir_str = self.public_dir.as_ref().as_ref().map(|path| path.to_string_lossy()); 45 | self.display.spice_args(vm_name, guest, public_dir_str) 46 | }) 47 | .transpose()?; 48 | 49 | let mouse = self.mouse.unwrap_or(guest.default_mouse()); 50 | let usb = usb_controller.usb_args(guest); 51 | 52 | Ok(( 53 | IoArgs { 54 | display, 55 | audio, 56 | mouse, 57 | usb, 58 | keyboard, 59 | keyboard_layout: self.keyboard_layout, 60 | #[cfg(not(target_os = "macos"))] 61 | spice, 62 | public_dir_args, 63 | }, 64 | warnings, 65 | )) 66 | } 67 | } 68 | 69 | pub struct IoArgs<'a> { 70 | display: DisplayArgs, 71 | audio: Audio, 72 | mouse: Mouse, 73 | usb: USBArgs, 74 | keyboard: Keyboard, 75 | keyboard_layout: KeyboardLayout, 76 | #[cfg(not(target_os = "macos"))] 77 | spice: Option>, 78 | public_dir_args: Option>, 79 | } 80 | 81 | impl EmulatorArgs for IoArgs<'_> { 82 | fn display(&self) -> impl IntoIterator { 83 | let iter = chain!( 84 | self.display.display(), 85 | self.audio.display(), 86 | self.mouse.display(), 87 | self.usb.display(), 88 | self.keyboard.display(), 89 | self.keyboard_layout.display(), 90 | self.public_dir_args.as_ref().map(|d| d.display()).into_iter().flatten(), 91 | ); 92 | 93 | #[cfg(not(target_os = "macos"))] 94 | let iter = iter.chain(self.spice.as_ref().map(|spice| spice.display()).into_iter().flatten()); 95 | 96 | iter 97 | } 98 | fn qemu_args(&self) -> impl IntoIterator { 99 | let iter = chain!( 100 | self.usb.qemu_args(), 101 | self.display.qemu_args(), 102 | self.audio.qemu_args(), 103 | self.mouse.qemu_args(), 104 | self.keyboard.qemu_args(), 105 | self.keyboard_layout.qemu_args(), 106 | self.public_dir_args.as_ref().map(|d| d.qemu_args()).into_iter().flatten(), 107 | ); 108 | 109 | #[cfg(not(target_os = "macos"))] 110 | let iter = iter.chain(self.spice.as_ref().map(|spice| spice.qemu_args()).into_iter().flatten()); 111 | 112 | iter 113 | } 114 | fn launch_fns(self) -> impl IntoIterator { 115 | let iter = chain!( 116 | self.display.launch_fns(), 117 | self.audio.launch_fns(), 118 | self.mouse.launch_fns(), 119 | self.usb.launch_fns(), 120 | self.keyboard.launch_fns(), 121 | self.keyboard_layout.launch_fns(), 122 | self.public_dir_args.map(|d| d.launch_fns()).into_iter().flatten(), 123 | ); 124 | 125 | #[cfg(not(target_os = "macos"))] 126 | let iter = iter.chain(self.spice.map(|spice| spice.launch_fns()).into_iter().flatten()); 127 | 128 | iter 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /quickemu/core/src/data/display.rs: -------------------------------------------------------------------------------- 1 | use super::{default_if_empty, is_default}; 2 | use serde::{de::Visitor, Deserialize, Serialize}; 3 | 4 | #[cfg(not(target_os = "macos"))] 5 | use std::net::{IpAddr, Ipv4Addr}; 6 | 7 | #[derive(Default, PartialEq, Clone, Debug, Serialize, Deserialize)] 8 | pub struct Display { 9 | #[serde(default, flatten, rename = "type")] 10 | #[serde(deserialize_with = "default_if_empty")] 11 | pub display_type: DisplayType, 12 | #[serde(default, skip_serializing_if = "is_default")] 13 | pub resolution: Resolution, 14 | #[serde(default, skip_serializing_if = "is_default")] 15 | pub accelerated: Accelerated, 16 | #[serde(default, skip_serializing_if = "is_default")] 17 | pub braille: bool, 18 | } 19 | 20 | #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] 21 | pub struct Accelerated(bool); 22 | 23 | impl From for bool { 24 | fn from(value: Accelerated) -> Self { 25 | value.0 26 | } 27 | } 28 | 29 | impl AsRef for Accelerated { 30 | fn as_ref(&self) -> &str { 31 | if self.0 { 32 | "on" 33 | } else { 34 | "off" 35 | } 36 | } 37 | } 38 | 39 | impl std::fmt::Display for Accelerated { 40 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 41 | let text = if self.0 { "Enabled" } else { "Disabled" }; 42 | write!(f, "{text}") 43 | } 44 | } 45 | 46 | impl Default for Accelerated { 47 | fn default() -> Self { 48 | Self(default_accel()) 49 | } 50 | } 51 | fn default_accel() -> bool { 52 | cfg!(not(target_os = "macos")) 53 | } 54 | 55 | impl Visitor<'_> for Accelerated { 56 | type Value = Accelerated; 57 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 58 | formatter.write_str("a boolean") 59 | } 60 | fn visit_bool(self, value: bool) -> Result 61 | where 62 | E: serde::de::Error, 63 | { 64 | Ok(Self(value)) 65 | } 66 | } 67 | 68 | #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] 69 | #[serde(tag = "type")] 70 | #[serde(rename_all = "snake_case")] 71 | pub enum Resolution { 72 | #[default] 73 | Default, 74 | #[cfg(feature = "display_resolution")] 75 | Display { 76 | display_name: Option, 77 | percentage: Option, 78 | }, 79 | Custom { 80 | width: u32, 81 | height: u32, 82 | }, 83 | FullScreen, 84 | } 85 | 86 | #[derive(Copy, Default, derive_more::Display, PartialEq, Clone, Debug, Serialize, Deserialize)] 87 | #[serde(tag = "type")] 88 | #[serde(rename_all = "snake_case")] 89 | pub enum DisplayType { 90 | None, 91 | #[serde(alias = "SDL")] 92 | #[display("SDL")] 93 | #[cfg_attr(not(target_os = "macos"), default)] 94 | Sdl, 95 | #[serde(alias = "GTK")] 96 | #[display("GTK")] 97 | Gtk, 98 | #[cfg(not(target_os = "macos"))] 99 | #[display("Spice")] 100 | #[serde(alias = "Spice")] 101 | Spice { 102 | #[serde(default, skip_serializing_if = "is_default")] 103 | access: Access, 104 | #[serde(default, skip_serializing_if = "is_default")] 105 | viewer: Viewer, 106 | #[serde(default = "default_spice_port", skip_serializing_if = "is_default_spice")] 107 | spice_port: u16, 108 | }, 109 | #[cfg(not(target_os = "macos"))] 110 | #[serde(alias = "Spice App", alias = "spice-app")] 111 | #[display("Spice App")] 112 | SpiceApp, 113 | #[cfg(target_os = "macos")] 114 | #[cfg_attr(target_os = "macos", default)] 115 | Cocoa, 116 | } 117 | const fn default_spice_port() -> u16 { 118 | 5930 119 | } 120 | fn is_default_spice(input: &u16) -> bool { 121 | *input == default_spice_port() 122 | } 123 | 124 | #[cfg(not(target_os = "macos"))] 125 | #[derive(Copy, derive_more::Display, PartialEq, Default, Deserialize, Serialize, Clone, Debug)] 126 | #[serde(rename_all = "snake_case")] 127 | pub enum Viewer { 128 | None, 129 | #[default] 130 | Spicy, 131 | Remote, 132 | } 133 | 134 | #[cfg(not(target_os = "macos"))] 135 | #[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize, derive_more::AsRef)] 136 | pub struct Access(Option); 137 | 138 | #[cfg(not(target_os = "macos"))] 139 | impl Default for Access { 140 | fn default() -> Self { 141 | local_access() 142 | } 143 | } 144 | 145 | #[cfg(not(target_os = "macos"))] 146 | fn local_access() -> Access { 147 | Access(Some(IpAddr::V4(Ipv4Addr::LOCALHOST))) 148 | } 149 | 150 | #[cfg(not(target_os = "macos"))] 151 | impl Visitor<'_> for Access { 152 | type Value = Access; 153 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 154 | formatter.write_str("an IP address, 'remote', or 'local'") 155 | } 156 | fn visit_str(self, value: &str) -> Result 157 | where 158 | E: serde::de::Error, 159 | { 160 | Ok(match value { 161 | "remote" => Self(None), 162 | "local" => local_access(), 163 | _ => { 164 | let address = value.parse().map_err(serde::de::Error::custom)?; 165 | Self(Some(address)) 166 | } 167 | }) 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /quickget/core/src/data_structures.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | pub use quickemu_core::data::{Arch, BootType, DiskFormat, GuestOS}; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Clone, Debug, Serialize, Deserialize)] 6 | pub struct OS { 7 | pub name: String, 8 | pub pretty_name: String, 9 | #[serde(default, skip_serializing_if = "Option::is_none")] 10 | pub homepage: Option, 11 | #[serde(default, skip_serializing_if = "Option::is_none")] 12 | pub description: Option, 13 | pub releases: Vec, 14 | } 15 | 16 | #[derive(Clone, Debug, Serialize, Deserialize)] 17 | pub struct Config { 18 | #[serde(default)] 19 | pub release: String, 20 | #[serde(default, skip_serializing_if = "Option::is_none")] 21 | pub edition: Option, 22 | #[serde(flatten, default, skip_serializing_if = "is_default", deserialize_with = "default_if_empty")] 23 | pub guest_os: GuestOS, 24 | #[serde(flatten, default, skip_serializing_if = "is_default", deserialize_with = "default_if_empty")] 25 | pub arch: Arch, 26 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 27 | pub iso: Vec, 28 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 29 | pub img: Vec, 30 | #[serde(default = "default_disk", skip_serializing_if = "is_default_disk")] 31 | pub disk_images: Option>, 32 | #[serde(default, skip_serializing_if = "is_default")] 33 | pub boot: BootType, 34 | #[serde(default, skip_serializing_if = "Option::is_none")] 35 | pub tpm: Option, 36 | #[serde(default, skip_serializing_if = "Option::is_none")] 37 | pub ram: Option, 38 | } 39 | 40 | impl Default for Config { 41 | fn default() -> Self { 42 | Self { 43 | release: "latest".to_string(), 44 | edition: None, 45 | guest_os: GuestOS::Linux, 46 | arch: Arch::default(), 47 | iso: Vec::new(), 48 | img: Vec::new(), 49 | disk_images: default_disk(), 50 | boot: Default::default(), 51 | tpm: None, 52 | ram: None, 53 | } 54 | } 55 | } 56 | fn is_default_disk(disk: &Option>) -> bool { 57 | disk == &default_disk() 58 | } 59 | fn default_disk() -> Option> { 60 | Some(vec![Default::default()]) 61 | } 62 | fn is_default(input: &T) -> bool { 63 | input == &T::default() 64 | } 65 | pub fn default_if_empty<'de, D, T>(deserializer: D) -> Result 66 | where 67 | D: serde::Deserializer<'de>, 68 | T: serde::Deserialize<'de> + Default, 69 | { 70 | let opt = Option::deserialize(deserializer)?; 71 | Ok(opt.unwrap_or_default()) 72 | } 73 | 74 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] 75 | pub struct Disk { 76 | pub source: Source, 77 | #[serde(default, skip_serializing_if = "Option::is_none")] 78 | pub size: Option, 79 | #[serde(default, flatten, skip_serializing_if = "is_default", deserialize_with = "default_if_empty")] 80 | pub format: DiskFormat, 81 | } 82 | impl Default for Disk { 83 | fn default() -> Self { 84 | Self { 85 | source: Source::FileName("disk.qcow2".to_string()), 86 | size: None, 87 | format: DiskFormat::default(), 88 | } 89 | } 90 | } 91 | 92 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] 93 | pub enum Source { 94 | #[serde(rename = "web")] 95 | Web(WebSource), 96 | #[serde(rename = "file_name")] 97 | FileName(String), 98 | #[serde(rename = "custom")] 99 | // Quickget will be required to manually handle "custom" sources. 100 | Custom, 101 | #[serde(rename = "docker")] 102 | Docker(DockerSource), 103 | } 104 | 105 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] 106 | pub struct DockerSource { 107 | pub url: String, 108 | pub privileged: bool, 109 | pub shared_dirs: Vec, 110 | pub output_filename: String, 111 | } 112 | 113 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] 114 | pub struct WebSource { 115 | pub url: String, 116 | #[serde(default, skip_serializing_if = "Option::is_none")] 117 | pub checksum: Option, 118 | #[serde(default, skip_serializing_if = "Option::is_none")] 119 | pub archive_format: Option, 120 | #[serde(default, skip_serializing_if = "Option::is_none")] 121 | pub file_name: Option, 122 | } 123 | impl WebSource { 124 | pub fn url_only(url: impl Into) -> Self { 125 | Self { 126 | url: url.into(), 127 | checksum: None, 128 | archive_format: None, 129 | file_name: None, 130 | } 131 | } 132 | pub fn new(url: String, checksum: Option, archive_format: Option, file_name: Option) -> Self { 133 | Self { 134 | url, 135 | checksum, 136 | archive_format, 137 | file_name, 138 | } 139 | } 140 | } 141 | 142 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] 143 | pub enum ArchiveFormat { 144 | #[serde(rename = "tar")] 145 | Tar, 146 | #[serde(rename = "tar.bz2")] 147 | TarBz2, 148 | #[serde(rename = "tar.gz")] 149 | TarGz, 150 | #[serde(rename = "tar.xz")] 151 | TarXz, 152 | #[serde(rename = "xz")] 153 | Xz, 154 | #[serde(rename = "gz")] 155 | Gz, 156 | #[serde(rename = "bz2")] 157 | Bz2, 158 | #[serde(rename = "zip")] 159 | Zip, 160 | #[serde(rename = "7z")] 161 | SevenZip, 162 | } 163 | -------------------------------------------------------------------------------- /quickemu/core/src/args/io/audio.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | arg, 3 | data::{Display, GuestOS, MacOSRelease, SoundCard, USBController}, 4 | error::{Error, Warning}, 5 | utils::{ArgDisplay, EmulatorArgs, QemuArg}, 6 | }; 7 | 8 | #[cfg(target_os = "linux")] 9 | use crate::data::DisplayType; 10 | 11 | impl GuestOS { 12 | pub(crate) fn default_soundcard(&self) -> SoundCard { 13 | match self { 14 | GuestOS::FreeDOS => SoundCard::SB16, 15 | GuestOS::Solaris => SoundCard::AC97, 16 | GuestOS::MacOS { release } if release >= &MacOSRelease::BigSur => SoundCard::USBAudio, 17 | _ => SoundCard::IntelHDA, 18 | } 19 | } 20 | } 21 | 22 | impl SoundCard { 23 | pub(crate) fn validate(&self, usb_controller: USBController) -> Result<(), Error> { 24 | if matches!(self, SoundCard::USBAudio) && usb_controller != USBController::Xhci { 25 | return Err(Error::ConflictingSoundUsb); 26 | } 27 | Ok(()) 28 | } 29 | } 30 | 31 | impl Display { 32 | pub(crate) fn audio(&self, sound_card: SoundCard) -> Result<(Audio, Option), Error> { 33 | let backend = match sound_card { 34 | SoundCard::None => AudioBackend::None, 35 | _ => match self.display_type { 36 | #[cfg(not(target_os = "macos"))] 37 | DisplayType::Spice { .. } | DisplayType::SpiceApp | DisplayType::None => AudioBackend::Spice, 38 | #[cfg(target_os = "macos")] 39 | _ => AudioBackend::CoreAudio, 40 | #[cfg(target_os = "windows")] 41 | _ => AudioBackend::DirectSound, 42 | #[cfg(target_os = "linux")] 43 | _ => { 44 | if process_active("pipewire") { 45 | #[cfg(not(feature = "qemu_8_1"))] 46 | { 47 | AudioBackend::PulseAudio 48 | } 49 | #[cfg(feature = "qemu_8_1")] 50 | AudioBackend::PipeWire 51 | } else if process_active("pulseaudio") { 52 | AudioBackend::PulseAudio 53 | } else { 54 | AudioBackend::Alsa 55 | } 56 | } 57 | #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] 58 | _ => AudioBackend::None, 59 | }, 60 | }; 61 | Ok((Audio { sound_card, backend }, None)) 62 | } 63 | } 64 | 65 | #[cfg(target_os = "linux")] 66 | fn process_active(name: &str) -> bool { 67 | let system = sysinfo::System::new_with_specifics(sysinfo::RefreshKind::new().with_processes(sysinfo::ProcessRefreshKind::new())); 68 | let process = system.processes_by_exact_name(name).next(); 69 | process.is_some() 70 | } 71 | 72 | pub(crate) struct Audio { 73 | sound_card: SoundCard, 74 | backend: AudioBackend, 75 | } 76 | 77 | enum AudioBackend { 78 | None, 79 | #[cfg(not(target_os = "macos"))] 80 | Spice, 81 | #[cfg(target_os = "macos")] 82 | CoreAudio, 83 | #[cfg(all(target_os = "linux", feature = "qemu_8_1"))] 84 | PipeWire, 85 | #[cfg(target_os = "linux")] 86 | PulseAudio, 87 | #[cfg(target_os = "linux")] 88 | Alsa, 89 | #[cfg(target_os = "windows")] 90 | DirectSound, 91 | } 92 | 93 | impl EmulatorArgs for Audio { 94 | fn display(&self) -> impl IntoIterator { 95 | let sound_type = match self.sound_card { 96 | SoundCard::None => "Disabled", 97 | SoundCard::AC97 => "AC97", 98 | SoundCard::ES1370 => "ES1370", 99 | SoundCard::SB16 => "Sound Blaster 16", 100 | SoundCard::IntelHDA => "Intel HDA", 101 | SoundCard::USBAudio => "USB Audio", 102 | }; 103 | Some(ArgDisplay { 104 | name: "Sound".into(), 105 | value: sound_type.into(), 106 | }) 107 | } 108 | fn qemu_args(&self) -> impl IntoIterator { 109 | let backend = match self.backend { 110 | AudioBackend::None => "none,id=audio0", 111 | #[cfg(not(target_os = "macos"))] 112 | AudioBackend::Spice => "spice,id=audio0", 113 | #[cfg(target_os = "macos")] 114 | AudioBackend::CoreAudio => "coreaudio,id=audio0", 115 | #[cfg(all(target_os = "linux", feature = "qemu_8_1"))] 116 | AudioBackend::PipeWire => "pipewire,id=audio0", 117 | #[cfg(target_os = "linux")] 118 | AudioBackend::PulseAudio => "pa,id=audio0", 119 | #[cfg(target_os = "linux")] 120 | AudioBackend::Alsa => "alsa,id=audio0", 121 | #[cfg(target_os = "windows")] 122 | AudioBackend::DirectSound => "dsound,id=audio0", 123 | }; 124 | 125 | let mut args = vec![arg!("-audiodev"), arg!(backend)]; 126 | 127 | match self.sound_card { 128 | SoundCard::None => {} 129 | SoundCard::AC97 => args.extend([arg!("-device"), arg!("ac97,audiodev=audio0")]), 130 | SoundCard::ES1370 => args.extend([arg!("-device"), arg!("es1370,audiodev=audio0")]), 131 | SoundCard::SB16 => args.extend([arg!("-device"), arg!("sb16,audiodev=audio0")]), 132 | SoundCard::USBAudio => args.extend([arg!("-device"), arg!("usb-audio,audiodev=audio0")]), 133 | SoundCard::IntelHDA => args.extend([arg!("-device"), arg!("intel-hda"), arg!("-device"), arg!("hda-duplex,audiodev=audio0")]), 134 | } 135 | 136 | args 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /quickemu/core/src/data/network.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | net::{IpAddr, Ipv4Addr, SocketAddr}, 3 | path::PathBuf, 4 | }; 5 | 6 | #[cfg(feature = "quickemu")] 7 | use crate::utils::find_port; 8 | #[cfg(feature = "quickemu")] 9 | use serde::de::Visitor; 10 | 11 | use super::{default_if_empty, is_default}; 12 | use serde::{Deserialize, Serialize}; 13 | 14 | #[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] 15 | pub struct Network { 16 | #[serde(default, flatten, deserialize_with = "default_if_empty")] 17 | pub network_type: NetworkType, 18 | #[serde(default, skip_serializing_if = "is_default")] 19 | pub monitor: Monitor, 20 | #[serde(default, skip_serializing_if = "is_default")] 21 | pub serial: Serial, 22 | } 23 | 24 | #[cfg_attr(not(feature = "quickemu"), derive(Default))] 25 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, derive_more::AsRef)] 26 | pub struct SSHPort(Option); 27 | #[cfg(feature = "quickemu")] 28 | impl Default for SSHPort { 29 | fn default() -> Self { 30 | Self(find_port(22220, 9)) 31 | } 32 | } 33 | 34 | #[cfg(feature = "quickemu")] 35 | impl Visitor<'_> for SSHPort { 36 | type Value = SSHPort; 37 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 38 | formatter.write_str("a port number") 39 | } 40 | fn visit_u16(self, value: u16) -> Result 41 | where 42 | E: serde::de::Error, 43 | { 44 | Ok(Self(find_port(value, 9))) 45 | } 46 | } 47 | 48 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, derive_more::AsRef)] 49 | pub struct Bridge(String); 50 | 51 | #[cfg(feature = "quickemu")] 52 | impl Visitor<'_> for Bridge { 53 | type Value = Bridge; 54 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 55 | formatter.write_str("a bridge device") 56 | } 57 | fn visit_str(self, value: &str) -> Result 58 | where 59 | E: serde::de::Error, 60 | { 61 | let networks = sysinfo::Networks::new_with_refreshed_list(); 62 | if !networks.contains_key(value) { 63 | return Err(E::custom(format!("Network interface {value} could not be found."))); 64 | } 65 | Ok(Self(value.to_string())) 66 | } 67 | } 68 | 69 | #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] 70 | pub struct PortForward { 71 | pub host: u16, 72 | pub guest: u16, 73 | } 74 | 75 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 76 | #[serde(tag = "type")] 77 | #[serde(rename_all = "snake_case")] 78 | pub enum NetworkType { 79 | None, 80 | #[serde(alias = "Bridged")] 81 | Bridged { 82 | bridge: Bridge, 83 | #[serde(default, alias = "MAC Address", alias = "macaddr", skip_serializing_if = "Option::is_none")] 84 | mac_addr: Option, 85 | }, 86 | #[serde(alias = "NAT")] 87 | Nat { 88 | #[serde(default, skip_serializing_if = "is_default")] 89 | port_forwards: Vec, 90 | #[serde(default, skip_serializing_if = "is_default")] 91 | ssh_port: SSHPort, 92 | #[serde(default, skip_serializing_if = "is_default")] 93 | restrict: bool, 94 | }, 95 | } 96 | 97 | impl Default for NetworkType { 98 | fn default() -> Self { 99 | Self::Nat { 100 | port_forwards: vec![], 101 | ssh_port: SSHPort::default(), 102 | restrict: false, 103 | } 104 | } 105 | } 106 | 107 | pub type Monitor = MonitorInner; 108 | pub type Serial = MonitorInner; 109 | 110 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 111 | #[serde(tag = "type")] 112 | #[serde(rename_all = "snake_case")] 113 | pub enum MonitorInner { 114 | None, 115 | #[serde(alias = "Telnet")] 116 | Telnet { 117 | #[serde(default)] 118 | address: T, 119 | }, 120 | #[cfg(unix)] 121 | #[serde(alias = "Socket")] 122 | Socket { 123 | socketpath: Option, 124 | }, 125 | } 126 | 127 | pub trait MonitorArg: Default + AsRef + AsMut { 128 | fn arg() -> &'static str; 129 | fn display() -> &'static str; 130 | } 131 | 132 | #[cfg(unix)] 133 | impl Default for MonitorInner { 134 | fn default() -> Self { 135 | Self::Socket { socketpath: None } 136 | } 137 | } 138 | 139 | #[cfg(not(unix))] 140 | impl Default for MonitorInner { 141 | fn default() -> Self { 142 | Self::Telnet { address: T::default() } 143 | } 144 | } 145 | 146 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, derive_more::AsRef, derive_more::AsMut)] 147 | pub struct MonitorAddr(SocketAddr); 148 | 149 | impl Default for MonitorAddr { 150 | fn default() -> Self { 151 | Self(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 4440)) 152 | } 153 | } 154 | 155 | impl MonitorArg for MonitorAddr { 156 | fn arg() -> &'static str { 157 | "-monitor" 158 | } 159 | fn display() -> &'static str { 160 | "Monitor" 161 | } 162 | } 163 | 164 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, derive_more::AsRef, derive_more::AsMut)] 165 | pub struct SerialAddr(SocketAddr); 166 | 167 | impl Default for SerialAddr { 168 | fn default() -> Self { 169 | Self(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 6660)) 170 | } 171 | } 172 | 173 | impl MonitorArg for SerialAddr { 174 | fn arg() -> &'static str { 175 | "-serial" 176 | } 177 | fn display() -> &'static str { 178 | "Serial" 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /quickget/cli/src/config.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use clap::ValueEnum; 3 | use itertools::Itertools; 4 | use quickemu_core::data::Arch; 5 | use quickget_core::{data_structures::Config, ConfigSearch, ConfigSearchError, QuickgetConfig}; 6 | use serde::Serialize; 7 | use std::io::{stdout, Write}; 8 | 9 | use crate::{fl, fl_bail}; 10 | 11 | async fn create_instance(refresh: bool) -> Result { 12 | if refresh { 13 | ConfigSearch::new_refreshed().await 14 | } else { 15 | ConfigSearch::new().await 16 | } 17 | } 18 | 19 | #[derive(Debug, Clone, ValueEnum)] 20 | pub enum ListType { 21 | Csv, 22 | Json, 23 | } 24 | 25 | #[derive(Serialize)] 26 | struct QuickgetList<'a> { 27 | #[serde(rename = "Display Name")] 28 | display_name: &'a str, 29 | #[serde(rename = "OS")] 30 | os: &'a str, 31 | #[serde(rename = "Release")] 32 | release: &'a str, 33 | #[serde(rename = "Option")] 34 | option: &'a str, 35 | #[serde(flatten)] 36 | #[serde(rename = "Arch")] 37 | arch: &'a Arch, 38 | #[serde(rename = "PNG")] 39 | png: String, 40 | #[serde(rename = "SVG")] 41 | svg: String, 42 | } 43 | 44 | pub async fn list(list_type: Option, refresh: bool) -> Result<()> { 45 | let instance = create_instance(refresh).await?; 46 | let empty_str = ""; 47 | let list = instance.get_os_list().iter().flat_map(|os| { 48 | os.releases.iter().map(|config| QuickgetList { 49 | display_name: os.pretty_name.as_str(), 50 | os: os.name.as_str(), 51 | release: config.release.as_str(), 52 | option: config.edition.as_deref().unwrap_or(empty_str), 53 | arch: &config.arch, 54 | png: format!( 55 | "https://quickemu-project.github.io/quickemu-icons/png/{}/{}-quickemu-white-pinkbg.png", 56 | os.name, os.name 57 | ), 58 | svg: format!( 59 | "https://quickemu-project.github.io/quickemu-icons/svg/{}/{}-quickemu-white-pinkbg.svg", 60 | os.name, os.name 61 | ), 62 | }) 63 | }); 64 | 65 | match list_type { 66 | Some(ListType::Csv) => { 67 | let mut wtr = csv::Writer::from_writer(stdout()); 68 | for item in list { 69 | wtr.serialize(item)?; 70 | } 71 | wtr.flush()?; 72 | } 73 | Some(ListType::Json) => { 74 | let list: Vec<_> = list.collect(); 75 | let mut stdout = stdout().lock(); 76 | serde_json::to_writer_pretty(&mut stdout, &list)?; 77 | stdout.flush()?; 78 | } 79 | None => { 80 | let mut stdout = stdout().lock(); 81 | for item in list { 82 | writeln!(&mut stdout, "{} {} {} {}", item.os, item.release, item.option, item.arch)?; 83 | } 84 | stdout.flush()?; 85 | } 86 | }; 87 | Ok(()) 88 | } 89 | 90 | pub async fn get(args: &[String], preferred_arch: Option<&Arch>, refresh: bool) -> Result { 91 | let mut instance = create_instance(refresh).await?; 92 | let mut args = args.iter(); 93 | 94 | if let Some(arch) = preferred_arch { 95 | instance.filter_arch_supported_os(arch)?; 96 | } 97 | 98 | let os = args 99 | .next() 100 | .with_context(|| fl!("unspecified-os", operating_systems = instance.list_os_names().join(" ")))?; 101 | let os = instance.filter_os(os)?; 102 | if let Some(arch) = preferred_arch { 103 | os.filter_arch(arch)?; 104 | } 105 | 106 | if let Some(release) = args.next() { 107 | instance.filter_release(release)?; 108 | } else { 109 | fl_bail!("unspecified-release", releases = list_releases(&os.releases, preferred_arch)); 110 | } 111 | 112 | let editions = instance.list_editions().unwrap(); 113 | if let Some(edition) = args.next() { 114 | instance.filter_edition(edition)?; 115 | } else if let Some(editions) = editions { 116 | fl_bail!("unspecified-edition", editions = editions.join(" ")); 117 | } 118 | 119 | instance.pick_best_match().map_err(Into::into) 120 | } 121 | 122 | fn list_releases(configs: &[Config], arch: Option<&Arch>) -> String { 123 | let release_text = fl!("releases"); 124 | let edition_text = fl!("editions"); 125 | let releases = configs 126 | .iter() 127 | .filter(|c| arch.is_none_or(|arch| c.arch == *arch)) 128 | .map(|c| c.release.as_str()) 129 | .unique() 130 | .collect::>(); 131 | let editions = releases 132 | .iter() 133 | .map(|r| editions_list(configs, r, arch)) 134 | .filter(|s| !s.is_empty()) 135 | .collect::>(); 136 | if editions.is_empty() { 137 | format!("{release_text}: {}", releases.join(" ")) 138 | } else if editions.iter().all_equal() { 139 | format!("{release_text}: {}\n{edition_text}: {}", releases.join(" "), editions[0]) 140 | } else { 141 | let max_len = releases.iter().map(|r| r.len()).max().unwrap_or_default(); 142 | let output = releases 143 | .iter() 144 | .map(|r| format!("{r:>() 146 | .join("\n"); 147 | 148 | format!("{release_text:) -> String { 153 | configs 154 | .iter() 155 | .filter(|c| { 156 | let conf_release = c.release.as_str(); 157 | conf_release.eq_ignore_ascii_case(release) && arch.is_none_or(|arch| *arch == c.arch) 158 | }) 159 | .map(|c| c.edition.as_deref().unwrap_or_default()) 160 | .unique() 161 | .collect::>() 162 | .join(" ") 163 | } 164 | -------------------------------------------------------------------------------- /quickemu/core/src/args/machine.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::OsString, path::Path}; 2 | 3 | use boot::BootArgs; 4 | use cpu::Cpu; 5 | use itertools::chain; 6 | use ram::Ram; 7 | use tpm::Tpm; 8 | 9 | use crate::{ 10 | arg, 11 | data::{AArch64Machine, Arch, BootType, GuestOS, Machine, Riscv64Machine, X86_64Machine}, 12 | error::{Error, Warning}, 13 | oarg, 14 | utils::{ArgDisplay, EmulatorArgs, LaunchFn, QemuArg}, 15 | }; 16 | 17 | mod boot; 18 | mod cpu; 19 | mod ram; 20 | mod tpm; 21 | 22 | impl Machine { 23 | pub fn args(&self, guest: GuestOS, vm_dir: &Path, vm_name: &str) -> Result<(MachineArgs, Vec), Error> { 24 | let mut warnings = Vec::new(); 25 | let (cpu_args, cpu_warnings) = self.cpu_args(guest)?; 26 | warnings.extend(cpu_warnings); 27 | 28 | let (ram_args, ram_warning) = self.ram_args(guest)?; 29 | warnings.extend(ram_warning); 30 | 31 | let tpm_args = self.tpm.then(|| Tpm::new(vm_dir, vm_name)).transpose()?; 32 | let boot_args = self.boot_args(vm_dir, guest)?; 33 | let machine_type = FullMachine::new(self.arch, guest, self.boot); 34 | 35 | Ok(( 36 | MachineArgs { 37 | cpu_args, 38 | ram_args, 39 | tpm_args, 40 | boot_args, 41 | machine_type, 42 | }, 43 | warnings, 44 | )) 45 | } 46 | } 47 | 48 | pub struct MachineArgs { 49 | cpu_args: Cpu, 50 | ram_args: Ram, 51 | tpm_args: Option, 52 | boot_args: BootArgs, 53 | machine_type: FullMachine, 54 | } 55 | 56 | impl EmulatorArgs for MachineArgs { 57 | fn display(&self) -> impl IntoIterator { 58 | chain!( 59 | self.cpu_args.display(), 60 | self.ram_args.display(), 61 | self.tpm_args.as_ref().map(|tpm| tpm.display()).into_iter().flatten(), 62 | self.boot_args.display(), 63 | self.machine_type.display(), 64 | ) 65 | } 66 | fn qemu_args(&self) -> impl IntoIterator { 67 | chain!( 68 | self.cpu_args.qemu_args(), 69 | self.ram_args.qemu_args(), 70 | self.tpm_args.as_ref().map(|tpm| tpm.qemu_args()).into_iter().flatten(), 71 | self.boot_args.qemu_args(), 72 | self.machine_type.qemu_args(), 73 | ) 74 | } 75 | fn launch_fns(self) -> impl IntoIterator { 76 | chain!( 77 | self.cpu_args.launch_fns(), 78 | self.ram_args.launch_fns(), 79 | self.tpm_args.map(|tpm| tpm.launch_fns()).into_iter().flatten(), 80 | self.boot_args.launch_fns(), 81 | self.machine_type.launch_fns(), 82 | ) 83 | } 84 | } 85 | 86 | struct FullMachine { 87 | qemu_machine: QemuMachineType, 88 | specific: MachineType, 89 | } 90 | 91 | enum MachineType { 92 | X86_64 { smm: bool, no_hpet: bool }, 93 | AArch64, 94 | Riscv64, 95 | } 96 | 97 | impl FullMachine { 98 | fn new(arch: Arch, guest: GuestOS, boot: BootType) -> Self { 99 | match arch { 100 | Arch::X86_64 { machine: X86_64Machine::Standard } => { 101 | // Secure boot on Linux may require SMM to be enabled 102 | // https://github.com/quickemu-project/quickemu/pull/1579 103 | let smm = matches!(guest, GuestOS::Windows | GuestOS::WindowsServer | GuestOS::FreeDOS) 104 | || (cfg!(target_os = "linux") && matches!((guest, boot), (GuestOS::Linux, BootType::Efi { secure_boot: true }))); 105 | let no_hpet = matches!(guest, GuestOS::Windows | GuestOS::WindowsServer | GuestOS::MacOS { .. }); 106 | let qemu_machine_type = match guest { 107 | GuestOS::FreeDOS | GuestOS::Batocera | GuestOS::Haiku | GuestOS::Solaris | GuestOS::ReactOS | GuestOS::KolibriOS => QemuMachineType::Pc, 108 | _ => QemuMachineType::Qemu32, 109 | }; 110 | Self { 111 | qemu_machine: qemu_machine_type, 112 | specific: MachineType::X86_64 { smm, no_hpet }, 113 | } 114 | } 115 | Arch::AArch64 { machine: AArch64Machine::Standard } => Self { 116 | qemu_machine: QemuMachineType::Virt, 117 | specific: MachineType::AArch64, 118 | }, 119 | Arch::Riscv64 { machine: Riscv64Machine::Standard } => Self { 120 | qemu_machine: QemuMachineType::Virt, 121 | specific: MachineType::Riscv64, 122 | }, 123 | } 124 | } 125 | } 126 | 127 | impl EmulatorArgs for FullMachine { 128 | fn qemu_args(&self) -> impl IntoIterator { 129 | let mut machine = self.qemu_machine.arg(); 130 | match self.specific { 131 | MachineType::X86_64 { smm, no_hpet } => { 132 | if smm { 133 | machine.push(",smm=on"); 134 | } else { 135 | machine.push(",smm=off"); 136 | } 137 | if no_hpet { 138 | machine.push(",hpet=off"); 139 | } 140 | machine.push(",vmport=off"); 141 | } 142 | MachineType::AArch64 => { 143 | machine.push(",virtualization=on,pflash0=rom,pflash1=efivars"); 144 | } 145 | MachineType::Riscv64 => { 146 | machine.push(",usb=on"); 147 | } 148 | } 149 | [arg!("-machine"), oarg!(machine)] 150 | } 151 | } 152 | 153 | enum QemuMachineType { 154 | Qemu32, 155 | Pc, 156 | Virt, 157 | } 158 | 159 | impl QemuMachineType { 160 | fn arg(&self) -> OsString { 161 | match self { 162 | Self::Qemu32 => "q35".into(), 163 | Self::Pc => "pc".into(), 164 | Self::Virt => "virt".into(), 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /quickemu/core/src/args/io/spice.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, process::Command}; 2 | 3 | use which::which; 4 | 5 | use crate::{ 6 | arg, 7 | data::{Accelerated, Access, Display, DisplayType, GuestOS, Resolution, Viewer}, 8 | error::Error, 9 | oarg, 10 | utils::{find_port, ArgDisplay, EmulatorArgs, LaunchFn, LaunchFnReturn, QemuArg}, 11 | }; 12 | 13 | impl<'a> Display { 14 | pub fn spice_args(&self, vm_name: &'a str, guest: GuestOS, public_dir: Option>) -> Result, Error> { 15 | match self.display_type { 16 | DisplayType::SpiceApp => Ok(SpiceArgs::SpiceApp { accelerated: self.accelerated }), 17 | DisplayType::Spice { access, spice_port, viewer } => { 18 | let Some(port) = find_port(spice_port, 9) else { 19 | return Err(Error::UnavailablePort(spice_port)); 20 | }; 21 | let public_dir = public_dir.and_then(|dir| (!matches!(guest, GuestOS::MacOS { .. })).then_some(dir)); 22 | let fullscreen = matches!(self.resolution, Resolution::FullScreen); 23 | Ok(SpiceArgs::Spice { 24 | fullscreen, 25 | vm_name, 26 | port, 27 | access, 28 | public_dir, 29 | viewer, 30 | }) 31 | } 32 | _ => unreachable!(), 33 | } 34 | } 35 | } 36 | 37 | pub enum SpiceArgs<'a> { 38 | SpiceApp { 39 | accelerated: Accelerated, 40 | }, 41 | Spice { 42 | fullscreen: bool, 43 | vm_name: &'a str, 44 | port: u16, 45 | access: Access, 46 | public_dir: Option>, 47 | viewer: Viewer, 48 | }, 49 | } 50 | 51 | impl EmulatorArgs for SpiceArgs<'_> { 52 | fn display(&self) -> impl IntoIterator { 53 | Some(match self { 54 | Self::SpiceApp { .. } => ArgDisplay { 55 | name: Cow::Borrowed("Spice"), 56 | value: Cow::Borrowed("Enabled"), 57 | }, 58 | Self::Spice { 59 | vm_name, 60 | port, 61 | public_dir, 62 | viewer, 63 | fullscreen, 64 | .. 65 | } => { 66 | let mut msg = match viewer { 67 | Viewer::None => return None, 68 | Viewer::Spicy => format!("spicy --title \"{vm_name}\" --port {port}"), 69 | Viewer::Remote => format!("remote-viewer --title \"{vm_name}\" \"spice://localhost:{port}\""), 70 | }; 71 | if let Some(public_dir) = public_dir { 72 | msg.extend([" --spice-shared-dir ", public_dir]); 73 | } 74 | if *fullscreen { 75 | msg.push_str(" --full-screen"); 76 | } 77 | ArgDisplay { 78 | name: Cow::Borrowed("Viewer (On host)"), 79 | value: Cow::Owned(msg), 80 | } 81 | } 82 | }) 83 | } 84 | 85 | fn qemu_args(&self) -> impl IntoIterator { 86 | let mut spice_arg = "disable-ticketing=on".to_string(); 87 | match self { 88 | Self::SpiceApp { accelerated } => { 89 | spice_arg.push_str(&format!(",gl={}", accelerated.as_ref())); 90 | } 91 | Self::Spice { port, access, .. } => { 92 | spice_arg.extend([",port=", &port.to_string()]); 93 | if let Some(address) = access.as_ref() { 94 | spice_arg.extend([",addr=", &address.to_string()]); 95 | } 96 | } 97 | } 98 | [arg!("-spice"), oarg!(spice_arg)] 99 | } 100 | 101 | fn launch_fns(self) -> impl IntoIterator { 102 | match self { 103 | Self::Spice { 104 | viewer, 105 | vm_name, 106 | public_dir, 107 | fullscreen, 108 | port, 109 | .. 110 | } => { 111 | if let Viewer::None = viewer { 112 | return None; 113 | } 114 | let vm_name = vm_name.to_string(); 115 | let public_dir = public_dir.map(|d| d.to_string()); 116 | 117 | let launch = move || { 118 | let viewer_cmd = match viewer { 119 | Viewer::Spicy => "spicy", 120 | Viewer::Remote => "remote-viewer", 121 | _ => unreachable!(), 122 | }; 123 | let cmd = which(viewer_cmd).map_err(|_| Error::ViewerNotFound(viewer_cmd))?; 124 | 125 | #[cfg(not(feature = "inbuilt_commands"))] 126 | let mut command = Command::new(cmd); 127 | 128 | command.arg("--title").arg(vm_name); 129 | match viewer { 130 | Viewer::Spicy => command.arg("--port").arg(port.to_string()), 131 | Viewer::Remote => command.arg(format!("spice://localhost:{port}")), 132 | _ => unreachable!(), 133 | }; 134 | 135 | if let Some(public_dir) = public_dir { 136 | command.arg("--spice-shared-dir"); 137 | command.arg(public_dir); 138 | } 139 | 140 | if fullscreen { 141 | command.arg("--full-screen"); 142 | } 143 | 144 | let child = command.spawn().map_err(|e| Error::Command(viewer_cmd, e.to_string()))?; 145 | 146 | Ok(vec![LaunchFnReturn::Process(child)]) 147 | }; 148 | 149 | Some(LaunchFn::After(Box::new(launch))) 150 | } 151 | _ => None, 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /quickemu/core/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use size::Size; 4 | 5 | use crate::{data::GuestOS, fl}; 6 | 7 | #[derive(derive_more::From, Debug)] 8 | pub enum ConfigError { 9 | Read(std::io::Error), 10 | Parse(toml::de::Error), 11 | LiveVM(LiveVMError), 12 | } 13 | 14 | impl std::error::Error for ConfigError {} 15 | impl fmt::Display for ConfigError { 16 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 17 | let text = match self { 18 | Self::Read(err) => fl!("read-config-error", err = err.to_string()), 19 | Self::Parse(err) => fl!("parse-config-error", err = err.to_string()), 20 | Self::LiveVM(err) => err.to_string(), 21 | }; 22 | f.write_str(&text) 23 | } 24 | } 25 | 26 | #[derive(Debug, Clone)] 27 | pub enum LiveVMError { 28 | LiveVMDe(String), 29 | DelLiveFile(String), 30 | VMKill(String), 31 | } 32 | 33 | impl std::error::Error for LiveVMError {} 34 | impl fmt::Display for LiveVMError { 35 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 36 | let text = match self { 37 | Self::LiveVMDe(err) => fl!("failed-live-vm-de", err = err), 38 | Self::DelLiveFile(err) => fl!("failed-del-live-file", err = err), 39 | Self::VMKill(err) => fl!("failed-vm-kill", err = err), 40 | }; 41 | f.write_str(&text) 42 | } 43 | } 44 | 45 | #[derive(derive_more::From, Debug, Clone)] 46 | pub enum Error { 47 | Instructions(&'static str), 48 | UnavailablePort(u16), 49 | InsufficientRam(Size, GuestOS), 50 | ConflictingSoundUsb, 51 | #[cfg(not(feature = "inbuilt_commands"))] 52 | #[from] 53 | Which(#[from] which::Error), 54 | Command(&'static str, String), 55 | LegacyBoot, 56 | Riscv64Bootloader, 57 | Ovmf, 58 | CopyOvmfVars(String), 59 | UnsupportedBootCombination, 60 | ViewerNotFound(&'static str), 61 | QemuNotFound(&'static str), 62 | DiskCreationFailed(String), 63 | DiskInUse(String), 64 | DeserializeQemuImgInfo(String), 65 | MacBootloader, 66 | NonexistentImage(String), 67 | MonitorCommand(String), 68 | FailedLiveVMSe(String), 69 | } 70 | 71 | impl std::error::Error for Error {} 72 | impl fmt::Display for Error { 73 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 74 | let text = match self { 75 | Self::Instructions(missing_instruction) => { 76 | let missing_instruction = *missing_instruction; 77 | fl!("macos-cpu-instructions", instruction = missing_instruction) 78 | } 79 | Self::UnavailablePort(port) => fl!("unavailable-port", port = port), 80 | Self::InsufficientRam(ram, guest) => fl!("insufficient-ram", ram = ram.to_string(), guest = guest.to_string()), 81 | Self::ConflictingSoundUsb => fl!("sound-usb-conflict"), 82 | #[cfg(not(feature = "inbuilt_commands"))] 83 | Self::Which(err) => fl!("which-binary", err = err.to_string()), 84 | Self::Command(bin, err) => { 85 | let bin = *bin; 86 | fl!("failed-launch", bin = bin, err = err) 87 | } 88 | Self::LegacyBoot => fl!("non-x86-bios"), 89 | Self::Riscv64Bootloader => fl!("riscv64-boot"), 90 | Self::Ovmf => fl!("efi-firmware"), 91 | Self::CopyOvmfVars(err) => fl!("failed-ovmf-copy", err = err), 92 | Self::UnsupportedBootCombination => fl!("unsupported-boot-combination"), 93 | Self::ViewerNotFound(requested_viewer) => { 94 | let requested_viewer = *requested_viewer; 95 | fl!("no-viewer", viewer_bin = requested_viewer) 96 | } 97 | Self::QemuNotFound(requested_qemu) => { 98 | let requested_qemu = *requested_qemu; 99 | fl!("no-qemu", qemu_bin = requested_qemu) 100 | } 101 | Self::DiskCreationFailed(err) => fl!("failed-disk-creation", err = err), 102 | Self::DiskInUse(disk) => fl!("disk-used", disk = disk), 103 | Self::DeserializeQemuImgInfo(err) => fl!("failed-qemu-img-deserialization", err = err), 104 | Self::MacBootloader => fl!("no-mac-bootloader"), 105 | Self::NonexistentImage(requested_image) => fl!("nonexistent-image", img = requested_image), 106 | Self::MonitorCommand(err) => fl!("monitor-command-failed", err = err), 107 | Self::FailedLiveVMSe(err) => fl!("failed-live-vm-se", err = err), 108 | }; 109 | f.write_str(&text) 110 | } 111 | } 112 | 113 | #[derive(Debug)] 114 | pub enum MonitorError { 115 | NoMonitor, 116 | Write(std::io::Error), 117 | Read(std::io::Error), 118 | } 119 | 120 | impl std::error::Error for MonitorError {} 121 | impl fmt::Display for MonitorError { 122 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 123 | let text = match self { 124 | Self::NoMonitor => fl!("no-monitor-available"), 125 | Self::Write(err) => fl!("failed-monitor-write", err = err.to_string()), 126 | Self::Read(err) => fl!("failed-monitor-read", err = err.to_string()), 127 | }; 128 | f.write_str(&text) 129 | } 130 | } 131 | 132 | #[derive(Debug, Clone)] 133 | pub enum Warning { 134 | MacOSCorePow2(usize), 135 | HwVirt(&'static str), 136 | #[cfg(target_os = "linux")] 137 | AudioBackend, 138 | InsufficientRamConfiguration(Size, GuestOS), 139 | } 140 | 141 | impl std::error::Error for Warning {} 142 | impl fmt::Display for Warning { 143 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 144 | let text = match self { 145 | Self::MacOSCorePow2(recommended) => fl!("macos-core-power-two", recommended = recommended), 146 | Self::HwVirt(virt_branding) => { 147 | let virt_branding = *virt_branding; 148 | fl!("software-virt-fallback", virt_branding = virt_branding) 149 | } 150 | #[cfg(target_os = "linux")] 151 | Self::AudioBackend => fl!("audio-backend-unavailable"), 152 | Self::InsufficientRamConfiguration(ram, guest) => fl!( 153 | "insufficient-ram-configuration", 154 | ram = ram.to_string(), 155 | guest = guest.to_string() 156 | ), 157 | }; 158 | f.write_str(&text) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /quickemu/core/src/args/io/display.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | arg, 3 | data::{Accelerated, Arch, Display, DisplayType, GuestOS, Resolution}, 4 | error::Error, 5 | oarg, 6 | utils::{ArgDisplay, EmulatorArgs, QemuArg}, 7 | }; 8 | 9 | impl Display { 10 | pub(crate) fn args(&self, guest: GuestOS, arch: Arch) -> Result { 11 | let gpu = match arch { 12 | Arch::X86_64 { .. } => match guest { 13 | GuestOS::Linux => match self.display_type { 14 | DisplayType::None => GpuType::VirtIOGPU, 15 | #[cfg(not(target_os = "macos"))] 16 | DisplayType::Spice { .. } | DisplayType::SpiceApp => GpuType::VirtIOGPU, 17 | _ => GpuType::VirtIOVGA, 18 | }, 19 | GuestOS::Windows | GuestOS::WindowsServer if self.display_type == DisplayType::Sdl => GpuType::VirtIOVGA, 20 | #[cfg(not(target_os = "macos"))] 21 | GuestOS::Windows | GuestOS::WindowsServer if matches!(self.display_type, DisplayType::SpiceApp) => GpuType::VirtIOVGA, 22 | #[cfg(target_os = "macos")] 23 | GuestOS::Windows | GuestOS::WindowsServer if self.display_type == DisplayType::Cocoa => GpuType::VirtIOVGA, 24 | GuestOS::Solaris | GuestOS::LinuxOld => GpuType::VMwareSVGA, 25 | _ => GpuType::Qxl, 26 | }, 27 | Arch::AArch64 { .. } => GpuType::VirtIOGPU, 28 | Arch::Riscv64 { .. } => GpuType::VirtIOVGA, 29 | }; 30 | 31 | let (fullscreen, res) = match &self.resolution { 32 | Resolution::FullScreen => (true, None), 33 | #[cfg(feature = "display_resolution")] 34 | Resolution::Default => (false, display_resolution(None, None)), 35 | #[cfg(not(feature = "display_resolution"))] 36 | Resolution::Default => (false, Some((1280, 800))), 37 | Resolution::Custom { width, height } => (false, Some((*width, *height))), 38 | #[cfg(feature = "display_resolution")] 39 | Resolution::Display { display_name, percentage } => (false, display_resolution(display_name.as_deref(), *percentage)), 40 | }; 41 | 42 | Ok(DisplayArgs { 43 | fullscreen, 44 | res, 45 | accelerated: self.accelerated, 46 | gpu, 47 | display: self.display_type, 48 | braille: self.braille, 49 | }) 50 | } 51 | } 52 | 53 | #[cfg(feature = "display_resolution")] 54 | fn display_resolution(name: Option<&str>, screenpct: Option) -> Option<(u32, u32)> { 55 | let display_info = display_info::DisplayInfo::all().ok()?; 56 | log::debug!("Displays: {display_info:?}"); 57 | let display = if let Some(monitor) = name { 58 | display_info.iter().find(|available| available.name == monitor) 59 | } else { 60 | display_info 61 | .iter() 62 | .find(|available| available.is_primary) 63 | .or(display_info.first()) 64 | }?; 65 | 66 | let (width, height) = match (display.width, display.height, screenpct) { 67 | (width, height, Some(screenpct)) => ( 68 | (screenpct * width as f64 / 100.0) as u32, 69 | (screenpct * height as f64 / 100.0) as u32, 70 | ), 71 | (3840.., 2160.., _) => (3200, 1800), 72 | (2560.., 1440.., _) => (2048, 1152), 73 | (1920.., 1080.., _) => (1664, 936), 74 | (1280.., 800.., _) => (1152, 648), 75 | (width, height, _) => (width, height), 76 | }; 77 | 78 | Some((width, height)) 79 | } 80 | 81 | pub(crate) struct DisplayArgs { 82 | res: Option<(u32, u32)>, 83 | fullscreen: bool, 84 | accelerated: Accelerated, 85 | gpu: GpuType, 86 | display: DisplayType, 87 | braille: bool, 88 | } 89 | 90 | #[derive(PartialEq, derive_more::Display)] 91 | enum GpuType { 92 | #[display("VirtIO VGA")] 93 | VirtIOVGA, 94 | #[display("VirtIO GPU")] 95 | VirtIOGPU, 96 | #[display("VMware SVGA")] 97 | VMwareSVGA, 98 | #[display("QXL")] 99 | Qxl, 100 | } 101 | 102 | impl EmulatorArgs for DisplayArgs { 103 | fn display(&self) -> impl IntoIterator { 104 | let resolution_text = if self.gpu != GpuType::VMwareSVGA && !self.fullscreen && self.res.is_some() { 105 | let (x, y) = self.res.unwrap(); 106 | format!(", Resolution: {x}x{y}") 107 | } else { 108 | "".into() 109 | }; 110 | Some(ArgDisplay { 111 | name: "Display".into(), 112 | value: format!( 113 | "{}, Device: {}, GL: {}{}", 114 | self.display, self.gpu, self.accelerated, resolution_text 115 | ) 116 | .into(), 117 | }) 118 | } 119 | fn qemu_args(&self) -> impl IntoIterator { 120 | let mut args = Vec::new(); 121 | let display_device_arg = match self.gpu { 122 | GpuType::VirtIOGPU => "virtio-gpu", 123 | GpuType::VirtIOVGA if self.accelerated.into() => "virtio-vga-gl", 124 | GpuType::VirtIOVGA => "virtio-vga", 125 | GpuType::VMwareSVGA => "vmware-svga,vgamem_mb=256", 126 | GpuType::Qxl => "qxl-vga,ram_size=65536,vram_size=65536,vgamem_mb=64", 127 | }; 128 | 129 | let display_type_arg = match self.display { 130 | DisplayType::Gtk => oarg!(format!("gtk,grab-on-hover=on,zoom-to-fit=off,gl={}", self.accelerated.as_ref())), 131 | DisplayType::None => arg!("none"), 132 | DisplayType::Sdl => oarg!(format!("sdl,gl={}", self.accelerated.as_ref())), 133 | #[cfg(not(target_os = "macos"))] 134 | DisplayType::Spice { .. } => arg!("none"), 135 | #[cfg(not(target_os = "macos"))] 136 | DisplayType::SpiceApp => oarg!(format!("spice-app,gl={}", self.accelerated.as_ref())), 137 | #[cfg(target_os = "macos")] 138 | DisplayType::Cocoa => arg!("cocoa"), 139 | }; 140 | args.extend([arg!("-display"), display_type_arg]); 141 | args.extend([arg!("-vga"), arg!("none")]); 142 | 143 | let display_device_arg = if self.fullscreen || self.gpu == GpuType::VMwareSVGA || self.res.is_none() { 144 | arg!(display_device_arg) 145 | } else { 146 | let (x, y) = self.res.unwrap(); 147 | oarg!(format!("{display_device_arg},xres={x},yres={y}")) 148 | }; 149 | args.extend([arg!("-device"), display_device_arg]); 150 | 151 | if self.fullscreen { 152 | args.push(arg!("-full-screen")); 153 | } 154 | 155 | if self.braille { 156 | args.extend([arg!("-chardev"), arg!("braille,id=brltty"), arg!("-device"), arg!("usb-braille,id=usbbrl,chardev=brltty")]); 157 | } 158 | args 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /quickemu/core/src/args/network.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, ffi::OsString, path::Path}; 2 | 3 | use itertools::chain; 4 | use which::which; 5 | 6 | use crate::{ 7 | arg, 8 | data::{GuestOS, MacOSRelease, Monitor, Network, NetworkType, PortForward, Serial}, 9 | error::{Error, Warning}, 10 | oarg, 11 | utils::{ArgDisplay, EmulatorArgs, LaunchFn, QemuArg}, 12 | }; 13 | 14 | mod monitor; 15 | 16 | impl<'a> Network { 17 | pub(crate) fn args(&'a self, guest: GuestOS, vm_name: &'a str, publicdir: Option<&'a Path>) -> Result<(FullNetworkArgs<'a>, Option), Error> { 18 | let network_args = self.inner_args(guest, vm_name, publicdir); 19 | Ok(( 20 | FullNetworkArgs { 21 | network: network_args, 22 | monitor: &self.monitor, 23 | serial: &self.serial, 24 | }, 25 | None, 26 | )) 27 | } 28 | 29 | fn inner_args(&'a self, guest: GuestOS, vm_name: &'a str, publicdir: Option<&'a Path>) -> NetworkArgs<'a> { 30 | let samba = if matches!(self.network_type, NetworkType::Nat { .. }) { 31 | which("smbd").ok().and(publicdir) 32 | } else { 33 | None 34 | }; 35 | NetworkArgs { 36 | network_type: &self.network_type, 37 | network_device: guest.into(), 38 | vm_name, 39 | samba, 40 | } 41 | } 42 | } 43 | 44 | pub(crate) struct FullNetworkArgs<'a> { 45 | network: NetworkArgs<'a>, 46 | monitor: &'a Monitor, 47 | serial: &'a Serial, 48 | } 49 | 50 | impl EmulatorArgs for FullNetworkArgs<'_> { 51 | fn display(&self) -> impl IntoIterator { 52 | chain!(self.network.display(), self.monitor.display(), self.serial.display()) 53 | } 54 | fn qemu_args(&self) -> impl IntoIterator { 55 | chain!(self.network.qemu_args(), self.monitor.qemu_args(), self.serial.qemu_args()) 56 | } 57 | fn launch_fns(self) -> impl IntoIterator { 58 | chain!(self.network.launch_fns()) 59 | } 60 | } 61 | 62 | struct NetworkArgs<'a> { 63 | network_type: &'a NetworkType, 64 | network_device: NetDevice, 65 | vm_name: &'a str, 66 | samba: Option<&'a Path>, 67 | } 68 | 69 | impl EmulatorArgs for NetworkArgs<'_> { 70 | fn display(&self) -> impl IntoIterator { 71 | let network_type = match self.network_type { 72 | NetworkType::None => Cow::Borrowed("Disabled"), 73 | NetworkType::Nat { restrict: true, .. } => Cow::Owned(format!("Restricted ({})", self.network_device)), 74 | NetworkType::Nat { restrict: false, .. } => Cow::Owned(format!("User ({})", self.network_device)), 75 | NetworkType::Bridged { bridge, .. } => Cow::Owned(format!("Bridged ({})", bridge.as_ref())), 76 | }; 77 | 78 | let network_msg = ArgDisplay { 79 | name: Cow::Borrowed("Network"), 80 | value: network_type, 81 | }; 82 | 83 | if let NetworkType::Nat { ssh_port, port_forwards, .. } = &self.network_type { 84 | let ssh_msg = match ssh_port.as_ref() { 85 | Some(port) => ArgDisplay { 86 | name: Cow::Borrowed("SSH (Host)"), 87 | value: Cow::Owned(format!("ssh {{user}}@localhost -p {port}")), 88 | }, 89 | None => ArgDisplay { 90 | name: Cow::Borrowed("SSH"), 91 | value: Cow::Borrowed("All ports exhausted"), 92 | }, 93 | }; 94 | 95 | let samba_msg = self.samba.map(|_| ArgDisplay { 96 | name: Cow::Borrowed("Samba (Guest)"), 97 | value: Cow::Borrowed("`smb://10.0.2.4/qemu`"), 98 | }); 99 | 100 | let port_forwards = port_forwards.iter().map(|PortForward { host, guest }| ArgDisplay { 101 | name: Cow::Borrowed("Port Forward"), 102 | value: Cow::Owned(format!("{host} => {guest}")), 103 | }); 104 | chain!(std::iter::once(network_msg), std::iter::once(ssh_msg), samba_msg, port_forwards).collect() 105 | } else { 106 | vec![network_msg] 107 | } 108 | } 109 | fn qemu_args(&self) -> impl IntoIterator { 110 | match &self.network_type { 111 | NetworkType::None => vec![arg!("-nic"), arg!("none")], 112 | NetworkType::Bridged { bridge, mac_addr } => { 113 | let mut nic = format!("bridge,br={}", bridge.as_ref()); 114 | if let Some(mac_addr) = mac_addr { 115 | nic.push_str(&format!(",mac={mac_addr}")); 116 | } 117 | vec![arg!("-nic"), oarg!(nic)] 118 | } 119 | NetworkType::Nat { ssh_port, port_forwards, restrict } => { 120 | let mut net = OsString::from("user,id=nic,hostname="); 121 | net.push(self.vm_name); 122 | if let Some(ssh_port) = ssh_port.as_ref() { 123 | net.push(",hostfwd=tcp::"); 124 | net.push(ssh_port.to_string()); 125 | net.push("-:22"); 126 | } 127 | if *restrict { 128 | net.push(",restrict=y"); 129 | } 130 | if let Some(samba) = self.samba { 131 | net.push(",smb="); 132 | net.push(samba); 133 | } 134 | for PortForward { host, guest } in port_forwards { 135 | net.push(",hostfwd=tcp::"); 136 | net.push(host.to_string()); 137 | net.push("-:"); 138 | net.push(guest.to_string()); 139 | } 140 | if let Some(samba) = self.samba { 141 | net.push(",smb="); 142 | net.push(samba); 143 | } 144 | 145 | vec![arg!("-netdev"), oarg!(net), arg!("-device"), arg!(self.network_device.arg())] 146 | } 147 | } 148 | } 149 | } 150 | 151 | #[derive(derive_more::Display)] 152 | enum NetDevice { 153 | E1000, 154 | VMXNET3, 155 | #[display("VirtIO Net")] 156 | VirtIONet, 157 | #[display("RTL 8139")] 158 | RTL8139, 159 | } 160 | 161 | impl NetDevice { 162 | fn arg(&self) -> &'static str { 163 | match self { 164 | Self::E1000 => "e1000,netdev=nic", 165 | Self::VMXNET3 => "vmxnet3,netdev=nic", 166 | Self::VirtIONet => "virtio-net,netdev=nic", 167 | Self::RTL8139 => "rtl8139,netdev=nic", 168 | } 169 | } 170 | } 171 | 172 | impl From for NetDevice { 173 | fn from(guest: GuestOS) -> Self { 174 | match guest { 175 | GuestOS::ReactOS => Self::E1000, 176 | GuestOS::MacOS { release } if release >= MacOSRelease::BigSur => Self::VirtIONet, 177 | GuestOS::MacOS { .. } => Self::VMXNET3, 178 | GuestOS::Linux | GuestOS::LinuxOld | GuestOS::Solaris | GuestOS::GhostBSD | GuestOS::FreeBSD | GuestOS::GenericBSD => Self::VirtIONet, 179 | _ => Self::RTL8139, 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /quickemu/core/src/data/io.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "quickemu")] 2 | use std::path::Path; 3 | use std::path::PathBuf; 4 | 5 | use super::{is_default, Display}; 6 | use serde::{de::Visitor, Deserialize, Serialize}; 7 | 8 | #[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] 9 | pub struct Io { 10 | #[serde(default, skip_serializing_if = "Option::is_none")] 11 | pub usb_controller: Option, 12 | #[serde(default, skip_serializing_if = "Option::is_none")] 13 | pub keyboard: Option, 14 | #[serde(default, skip_serializing_if = "is_default")] 15 | pub keyboard_layout: KeyboardLayout, 16 | #[serde(default, skip_serializing_if = "Option::is_none")] 17 | pub mouse: Option, 18 | #[serde(default, skip_serializing_if = "Option::is_none")] 19 | pub soundcard: Option, 20 | #[serde(default, skip_serializing_if = "is_default")] 21 | pub display: Display, 22 | #[serde(default, skip_serializing_if = "is_default")] 23 | pub public_dir: PublicDir, 24 | } 25 | 26 | impl Io { 27 | #[cfg(feature = "quickemu")] 28 | pub(crate) fn public_dir(&self) -> Option<&Path> { 29 | self.public_dir.as_ref().as_deref() 30 | } 31 | } 32 | 33 | #[derive(PartialEq, Default, Debug, Deserialize, Serialize, derive_more::AsRef, Clone)] 34 | pub struct USBDevices(Option>); 35 | 36 | #[cfg_attr(not(feature = "quickemu"), derive(Default))] 37 | #[derive(PartialEq, Clone, Debug, Deserialize, Serialize, derive_more::AsRef)] 38 | pub struct PublicDir(Option); 39 | 40 | #[cfg(feature = "quickemu")] 41 | impl Default for PublicDir { 42 | fn default() -> Self { 43 | let public_dir = dirs::public_dir(); 44 | let home_dir = dirs::home_dir(); 45 | 46 | // If the default public dir is the user's home directory, we won't share it with the guest 47 | // for security reasons 48 | if home_dir != public_dir { 49 | Self(public_dir) 50 | } else { 51 | Self(None) 52 | } 53 | } 54 | } 55 | 56 | impl Visitor<'_> for PublicDir { 57 | type Value = PublicDir; 58 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 59 | formatter.write_str("a valid path, 'default', or 'none'") 60 | } 61 | fn visit_str(self, value: &str) -> Result 62 | where 63 | E: serde::de::Error, 64 | { 65 | match value { 66 | "default" => Ok(Self::default()), 67 | "none" => Ok(Self(None)), 68 | _ => { 69 | let path = PathBuf::from(value); 70 | if !path.is_dir() { 71 | return Err(serde::de::Error::custom(format!("Path '{value}' is not a directory"))); 72 | } 73 | Ok(Self(Some(path))) 74 | } 75 | } 76 | } 77 | } 78 | 79 | #[derive(PartialEq, Copy, Clone, Debug, Serialize, Deserialize)] 80 | #[serde(rename_all = "snake_case")] 81 | pub enum USBController { 82 | None, 83 | #[serde(alias = "EHCI")] 84 | Ehci, 85 | #[serde(alias = "XHCI")] 86 | Xhci, 87 | } 88 | 89 | #[derive(PartialEq, Copy, Clone, Debug, Serialize, Deserialize)] 90 | #[serde(rename_all = "snake_case")] 91 | pub enum Mouse { 92 | #[serde(alias = "USB")] 93 | Usb, 94 | Tablet, 95 | Virtio, 96 | #[serde(alias = "PS2")] 97 | PS2, 98 | } 99 | 100 | #[derive(Copy, PartialEq, Clone, Debug, Serialize, Deserialize)] 101 | #[serde(rename_all = "snake_case")] 102 | pub enum SoundCard { 103 | None, 104 | #[serde(alias = "Intel HDA")] 105 | IntelHDA, 106 | #[serde(alias = "AC97")] 107 | AC97, 108 | #[serde(alias = "ES1370")] 109 | ES1370, 110 | #[serde(alias = "SB16")] 111 | SB16, 112 | #[serde(alias = "USB Audio")] 113 | USBAudio, 114 | } 115 | 116 | #[derive(Copy, Default, PartialEq, Clone, Debug, Serialize, Deserialize)] 117 | #[serde(rename_all = "snake_case")] 118 | pub enum Keyboard { 119 | #[default] 120 | #[serde(alias = "USB")] 121 | Usb, 122 | Virtio, 123 | #[serde(alias = "PS2")] 124 | PS2, 125 | } 126 | 127 | #[derive(derive_more::Display, Copy, PartialEq, Default, Clone, Debug, Serialize, Deserialize)] 128 | #[serde(rename_all = "snake_case")] 129 | pub enum KeyboardLayout { 130 | #[serde(alias = "ar")] 131 | Arabic, 132 | #[serde(alias = "de-ch")] 133 | SwissGerman, 134 | #[serde(alias = "es")] 135 | Spanish, 136 | #[serde(alias = "fo")] 137 | Faroese, 138 | #[serde(alias = "fr-ca")] 139 | FrenchCanadian, 140 | #[serde(alias = "hu")] 141 | Hungarian, 142 | #[serde(alias = "ja")] 143 | Japanese, 144 | #[serde(alias = "mk")] 145 | Macedonian, 146 | #[serde(alias = "no")] 147 | Norwegian, 148 | #[serde(alias = "pt-br")] 149 | BrazilianPortuguese, 150 | #[serde(alias = "sv")] 151 | Swedish, 152 | #[serde(alias = "da")] 153 | Danish, 154 | #[serde(alias = "en-gb")] 155 | BritishEnglish, 156 | #[serde(alias = "et")] 157 | Estonian, 158 | #[serde(alias = "fr")] 159 | French, 160 | #[serde(alias = "fr-ch")] 161 | SwissFrench, 162 | #[serde(alias = "is")] 163 | Icelandic, 164 | #[serde(alias = "lt")] 165 | Lithuanian, 166 | #[serde(alias = "nl")] 167 | Dutch, 168 | #[serde(alias = "pl")] 169 | Polish, 170 | #[serde(alias = "ru")] 171 | Russian, 172 | #[serde(alias = "th")] 173 | Thai, 174 | #[serde(alias = "de")] 175 | German, 176 | #[serde(alias = "en-us")] 177 | #[default] 178 | AmericanEnglish, 179 | #[serde(alias = "fi")] 180 | Finnish, 181 | #[serde(alias = "fr-be")] 182 | BelgianFrench, 183 | #[serde(alias = "hr")] 184 | Croatian, 185 | #[serde(alias = "it")] 186 | Italian, 187 | #[serde(alias = "lv")] 188 | Latvian, 189 | #[serde(alias = "nb")] 190 | NorwegianBokmal, 191 | #[serde(alias = "pt")] 192 | Portuguese, 193 | #[serde(alias = "sl")] 194 | Slovenian, 195 | #[serde(alias = "tr")] 196 | Turkish, 197 | } 198 | 199 | impl KeyboardLayout { 200 | pub fn as_str(&self) -> &'static str { 201 | match self { 202 | Self::Arabic => "ar", 203 | Self::SwissGerman => "de-ch", 204 | Self::Spanish => "es", 205 | Self::Faroese => "fo", 206 | Self::FrenchCanadian => "fr-ca", 207 | Self::Hungarian => "hu", 208 | Self::Japanese => "ja", 209 | Self::Macedonian => "mk", 210 | Self::Norwegian => "no", 211 | Self::BrazilianPortuguese => "pt-br", 212 | Self::Swedish => "sv", 213 | Self::Danish => "da", 214 | Self::BritishEnglish => "en-gb", 215 | Self::Estonian => "et", 216 | Self::French => "fr", 217 | Self::SwissFrench => "fr-ch", 218 | Self::Icelandic => "is", 219 | Self::Lithuanian => "lt", 220 | Self::Dutch => "nl", 221 | Self::Polish => "pl", 222 | Self::Russian => "ru", 223 | Self::Thai => "th", 224 | Self::German => "de", 225 | Self::AmericanEnglish => "en-us", 226 | Self::Finnish => "fi", 227 | Self::BelgianFrench => "fr-be", 228 | Self::Croatian => "hr", 229 | Self::Italian => "it", 230 | Self::Latvian => "lv", 231 | Self::NorwegianBokmal => "nb", 232 | Self::Portuguese => "pt", 233 | Self::Slovenian => "sl", 234 | Self::Turkish => "tr", 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /quickemu/core/src/args/machine/boot.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, 3 | ffi::OsString, 4 | path::{Path, PathBuf}, 5 | }; 6 | 7 | use crate::{ 8 | arg, 9 | data::{Arch, BootType, GuestOS, Machine}, 10 | error::Error, 11 | oarg, 12 | utils::{ArgDisplay, EmulatorArgs, QemuArg}, 13 | }; 14 | 15 | const SECURE_BOOT_OVMF: &[(&str, &str)] = &[ 16 | ("OVMF/OVMF_CODE_4M.secboot.fd", "OVMF/OVMF_VARS_4M.ms.fd"), 17 | ("edk2/ovmf/OVMF_CODE.secboot.fd", "edk2/ovmf/OVMF_VARS.secboot.fd"), 18 | ("OVMF/x64/OVMF_CODE.secboot.fd", "OVMF/x64/OVMF_VARS.fd"), 19 | ("edk2-ovmf/OVMF_CODE.secboot.fd", "edk2-ovmf/OVMF_VARS.fd"), 20 | ("qemu/ovmf-x86_64-smm-ms-code.bin", "qemu/ovmf-x86_64-smm-ms-vars.bin"), 21 | ("qemu/edk2-x86_64-secure-code.fd", "qemu/edk2-x86_64-code.fd"), 22 | ("edk2-ovmf/x64/OVMF_CODE.secboot.fd", "edk2-ovmf/x64/OVMF_VARS.fd"), 23 | ("edk2/x64/OVMF_CODE.secboot.4m.fd", "edk2/x64/OVMF_VARS.4m.fd"), 24 | ]; 25 | const EFI_OVMF: &[(&str, &str)] = &[ 26 | ("OVMF/OVMF_CODE_4M.fd", "OVMF/OVMF_VARS_4M.fd"), 27 | ("edk2/ovmf/OVMF_CODE.fd", "edk2/ovmf/OVMF_VARS.fd"), 28 | ("OVMF/OVMF_CODE.fd", "OVMF/OVMF_VARS.fd"), 29 | ("OVMF/x64/OVMF_CODE.fd", "OVMF/x64/OVMF_VARS.fd"), 30 | ("edk2-ovmf/OVMF_CODE.fd", "edk2-ovmf/OVMF_VARS.fd"), 31 | ("qemu/ovmf-x86_64-4m-code.bin", "qemu/ovmf-x86_64-4m-vars.bin"), 32 | ("qemu/edk2-x86_64-code.fd", "qemu/edk2-x86_64-code.fd"), 33 | ("edk2-ovmf/x64/OVMF_CODE.fd", "edk2-ovmf/x64/OVMF_VARS.fd"), 34 | ("edk2/x64/OVMF_CODE.4m.fd", "edk2/x64/OVMF_VARS.4m.fd"), 35 | ]; 36 | const AARCH64_OVMF: [(&str, &str); 1] = [("AAVMF/AAVMF_CODE.fd", "AAVMF/AAVMF_VARS.fd")]; 37 | const RISCV64_UBOOT: [&str; 1] = ["/usr/lib/u-boot/qemu-riscv64_smode/u-boot.bin"]; 38 | 39 | impl Machine { 40 | pub(crate) fn boot_args(&self, vm_dir: &Path, guest: GuestOS) -> Result { 41 | match (&self.boot, self.arch) { 42 | (BootType::Efi { secure_boot: false }, Arch::X86_64 { .. }) if matches!(guest, GuestOS::MacOS { .. }) => macos_firmware(vm_dir), 43 | _ if matches!(guest, GuestOS::MacOS { .. }) => Err(Error::UnsupportedBootCombination), 44 | (BootType::Legacy, Arch::X86_64 { .. }) => Ok(BootArgs::X86_64Bios), 45 | (BootType::Legacy, _) => Err(Error::LegacyBoot), 46 | (BootType::Efi { secure_boot: _ }, Arch::Riscv64 { .. }) => find_riscv64_bios(vm_dir), 47 | (BootType::Efi { secure_boot }, Arch::X86_64 { .. }) => { 48 | let ovmf = if *secure_boot { SECURE_BOOT_OVMF } else { EFI_OVMF }; 49 | standard_firmware(vm_dir, *secure_boot, ovmf).map(BootArgs::X86_64Efi) 50 | } 51 | (BootType::Efi { secure_boot: false }, Arch::AArch64 { .. }) => standard_firmware(vm_dir, false, AARCH64_OVMF.as_slice()).map(BootArgs::AArch64Efi), 52 | _ => Err(Error::UnsupportedBootCombination), 53 | } 54 | } 55 | } 56 | 57 | pub(crate) enum BootArgs { 58 | X86_64Bios, 59 | X86_64Efi(Efi), 60 | AArch64Efi(Efi), 61 | Riscv64Efi(PathBuf), 62 | } 63 | 64 | pub(crate) struct Efi { 65 | code: PathBuf, 66 | vars: PathBuf, 67 | secure_boot: bool, 68 | } 69 | 70 | impl EmulatorArgs for BootArgs { 71 | fn display(&self) -> impl IntoIterator { 72 | let value = match self { 73 | Self::X86_64Bios => Cow::Borrowed("Legacy/BIOS"), 74 | Self::X86_64Efi(Efi { code, secure_boot, .. }) => Cow::Owned(format!( 75 | "EFI (x86_64), OVMF: {}, Secure Boot: {}", 76 | code.display(), 77 | if *secure_boot { "Enabled" } else { "Disabled" } 78 | )), 79 | Self::AArch64Efi(Efi { code, .. }) => Cow::Owned(format!("EFI (AArch64), OVMF: {}", code.display())), 80 | Self::Riscv64Efi(bootloader) => Cow::Owned(format!("EFI (Riscv64), Bootloader: {}", bootloader.display())), 81 | }; 82 | Some(ArgDisplay { name: Cow::Borrowed("Boot"), value }) 83 | } 84 | fn qemu_args(&self) -> impl IntoIterator { 85 | match self { 86 | Self::X86_64Bios => vec![], 87 | Self::X86_64Efi(Efi { code, vars, .. }) => { 88 | let mut ovmf_code_final = OsString::from("if=pflash,format=raw,unit=0,file="); 89 | ovmf_code_final.push(code); 90 | ovmf_code_final.push(",readonly=on"); 91 | let mut ovmf_vars_final = OsString::from("if=pflash,format=raw,unit=1,file="); 92 | ovmf_vars_final.push(vars); 93 | vec![ 94 | arg!("-global"), 95 | arg!("driver=cfi.pflash01,property=secure,value=on"), 96 | arg!("-drive"), 97 | oarg!(ovmf_code_final), 98 | arg!("-drive"), 99 | oarg!(ovmf_vars_final), 100 | ] 101 | } 102 | Self::AArch64Efi(Efi { code, vars, .. }) => { 103 | let mut aavmf_code_final = OsString::from("node-name=rom,driver=file,filename="); 104 | aavmf_code_final.push(code); 105 | aavmf_code_final.push(",read-only=true"); 106 | let mut aavmf_vars_final = OsString::from("node-name=efivars,driver=file,filename="); 107 | aavmf_vars_final.push(vars); 108 | vec![arg!("-blockdev"), oarg!(aavmf_code_final), arg!("-blockdev"), oarg!(aavmf_vars_final)] 109 | } 110 | Self::Riscv64Efi(bootloader) => { 111 | vec![arg!("-kernel"), oarg!(bootloader)] 112 | } 113 | } 114 | } 115 | } 116 | 117 | fn find_riscv64_bios(vm_dir: &Path) -> Result { 118 | let bios_dirs = [&vm_dir.join("bios"), vm_dir]; 119 | let bios = bios_dirs 120 | .into_iter() 121 | .filter_map(|dir| dir.read_dir().ok()) 122 | .flat_map(|dir| dir.flatten().map(|file| file.path())) 123 | .find(|path| path.extension().is_some_and(|ext| ext == "bin")); 124 | 125 | bios.or_else(|| { 126 | RISCV64_UBOOT 127 | .iter() 128 | .map(Path::new) 129 | .find(|path| path.exists()) 130 | .map(|path| path.to_path_buf()) 131 | }) 132 | .map(BootArgs::Riscv64Efi) 133 | .ok_or(Error::Riscv64Bootloader) 134 | } 135 | 136 | fn qemu_share_dir() -> PathBuf { 137 | #[cfg(target_os = "macos")] 138 | if let Ok(output) = std::process::Command::new("brew").arg("--prefix").arg("qemu").output() { 139 | if output.status.success() { 140 | if let Ok(prefix) = std::str::from_utf8(&output.stdout) { 141 | log::debug!("Found QEMU prefix: {}", prefix); 142 | return PathBuf::from(prefix.trim()).join("share"); 143 | } 144 | } 145 | } 146 | PathBuf::from("/usr/share") 147 | } 148 | 149 | fn standard_firmware(vm_dir: &Path, secure_boot: bool, ovmfs: &[(&str, &str)]) -> Result { 150 | let vm_vars = vm_dir.join("OVMF_VARS.fd"); 151 | 152 | let share_dir = qemu_share_dir(); 153 | ovmfs 154 | .iter() 155 | .map(|(code, vars)| (share_dir.join(code), share_dir.join(vars))) 156 | .find(|(code, vars)| code.exists() && vars.exists()) 157 | .map(|(code, vars)| { 158 | if !vm_vars.exists() || vm_vars.metadata().is_ok_and(|m| m.permissions().readonly()) { 159 | std::fs::copy(vars, &vm_vars).map_err(|e| Error::CopyOvmfVars(e.to_string()))?; 160 | } 161 | let code = code.canonicalize().expect("OVMF Code should be a valid path"); 162 | Ok::<_, Error>((code, vm_vars)) 163 | }) 164 | .transpose()? 165 | .map(|(code, vars)| Efi { code, vars, secure_boot }) 166 | .ok_or(Error::Ovmf) 167 | } 168 | 169 | fn macos_firmware(vm_dir: &Path) -> Result { 170 | let code = vm_dir.join("OVMF_CODE.fd"); 171 | if !code.exists() { 172 | return Err(Error::Ovmf); 173 | } 174 | let vars = ["OVMF_VARS-1024x768.fd", "OVMF_VARS-1920x1080.fd"] 175 | .iter() 176 | .map(|vars| vm_dir.join(vars)) 177 | .find(|vars| vars.exists()) 178 | .ok_or(Error::Ovmf)?; 179 | 180 | Ok(BootArgs::X86_64Efi(Efi { code, vars, secure_boot: false })) 181 | } 182 | -------------------------------------------------------------------------------- /docs/src/configuration/configuration.md: -------------------------------------------------------------------------------- 1 | Quickemu can be configured primarily by editing the configuration file, which is TOML formatted. 2 | 3 | # Guest OS 4 | 5 | Specifying a Guest OS allows quickemu to select compatible hardware devices, optimize performance, and 6 | enable features supported on different guest operating systems. 7 | 8 | The field can be edited as so. 9 | 10 | ```toml 11 | [guest] 12 | os = "macos" 13 | # On certain operating systems, other fields can be present here. 14 | # For example, macOS requires a release to select compatible emulated hardware. 15 | release = "sequoia" 16 | ``` 17 | 18 | # VM Directory and Name 19 | 20 | The VM's name can be set through the 'vm_name' entry in the configuration file. 21 | 22 | The VM's directory can be set through the 'vm_dir' entry. 23 | 24 | If either are unpopulated, they will be set based on the other, or if unavailable, 25 | the configuration file's name. 26 | 27 | # Machine configuration 28 | 29 | Quickemu allows for various customizations to the emulated machine. 30 | 31 | All options within this category must be under [machine] in your configuration file. 32 | 33 | ## CPU threads 34 | 35 | Quickemu will automatically detect whether your CPU supports SMT, and 36 | configure your VM in the same way. On a system with SMT, an odd number of threads 37 | will be rounded down. 38 | 39 | You can set the amount of total CPU threads the VM should receive as such. 40 | 41 | ```toml 42 | cpu_threads = 8 43 | ``` 44 | 45 | ## RAM 46 | 47 | Quickemu supports configuring sizes, such as RAM, using both integers (in bytes), as well as 48 | strings representing a size, with binary-prefix units. 49 | 50 | For example, the configuration below will allocate 8 GiB of memory to your VM. 51 | 52 | ```toml 53 | ram = "8G" 54 | ``` 55 | 56 | ## Boot Types 57 | 58 | Quickemu defaults to EFI boot with secure boot disabled. 59 | Legacy BIOS and Secure Boot are both supported. 60 | 61 | You can configure quickemu to use legacy BIOS as follows 62 | 63 | ```toml 64 | [machine.boot] 65 | type = "legacy" 66 | ``` 67 | 68 | Alternatively, to enable secure boot, you can modify the configuration as follows. 69 | Note that you must explictly specify the EFI boot type in order to enable secure boot. 70 | 71 | ```toml 72 | [machine.boot] 73 | type = "efi" 74 | secure_boot = true 75 | ``` 76 | 77 | ## TPM 78 | 79 | Quickemu supports TPM 2.0, using swtpm for emulation. It can be configured as follows 80 | 81 | ```toml 82 | tpm = true 83 | ``` 84 | 85 | ## Status Quo 86 | 87 | This option marks all disks attached to the VM as read-only, 88 | ensuring they are not modified while the VM is running. 89 | 90 | ```toml 91 | status_quo = true 92 | ``` 93 | 94 | # IO devices 95 | 96 | Quickemu supports configuration of the IO devices emulated to the guest. 97 | 98 | All options within this category must be under [io] in your configuration file. 99 | 100 | ## Overriding Guest-selected IO devices 101 | 102 | ### USB Controller 103 | 104 | Certain guests (in specific, macOS) may break with non-default USB controllers. 105 | You can also disable the USB controller: 106 | 107 | ```toml 108 | usb_controller = "none" 109 | ``` 110 | 111 | ### Keyboard 112 | 113 | ```toml 114 | keyboard = "virtio" 115 | ``` 116 | 117 | ### Mouse 118 | 119 | ```toml 120 | mouse = "tablet" 121 | ``` 122 | 123 | ### Sound Card 124 | 125 | ```toml 126 | sound_card = "intel_hda" 127 | ``` 128 | 129 | ## Keyboard Layout 130 | 131 | Quickemu, through QEMU, supports emulating various keyboard layouts to the guest. 132 | The default is 'en-us' 133 | 134 | ```toml 135 | keyboard_layout = "fr" 136 | ``` 137 | 138 | ## Public Directory 139 | 140 | You can modify which directory is shared with the VM. The default currently is 141 | ~/Public. 142 | You can also disable sharing a directory: 143 | 144 | ```toml 145 | public_dir = "none" 146 | ``` 147 | 148 | # Display 149 | 150 | Display is a subcategory of io. Therefore, all display configuration must be put under [io.display] 151 | 152 | ## Display Types 153 | 154 | Available display types are as follows 155 | 156 | none, sdl, gtk, spice, spice_app, cocoa 157 | 158 | Cocoa is specific to macOS, while quickemu builds without spice support on 159 | macOS targets due to the lack of spice support in the homebrew QEMU package. 160 | 161 | ### Spice 162 | 163 | Spice displays (not to be confused with spice app) have certain other configuration options. 164 | 165 | ```toml 166 | type = "spice" 167 | # The port spice can be accessed through 168 | spice_port = 5930 169 | # The IP address spice can be accessed through 170 | access = "127.0.0.1" 171 | # The spice viewer for quickemu to launch after the VM starts 172 | viewer = "remote" 173 | ``` 174 | 175 | ## Resolution 176 | 177 | Resolution can be set in multiple ways. 178 | 179 | You can fully customize it with a width & height as follows: 180 | 181 | ```toml 182 | [io.display.resolution] 183 | type = "custom" 184 | width = 1920 185 | height = 1080 186 | ``` 187 | 188 | You can also enable fullscreen 189 | 190 | ```toml 191 | [io.display.resolution] 192 | type = "fullscreen" 193 | ``` 194 | 195 | Or (with the display_resolution feature flag - enabled by default): 196 | 197 | ```toml 198 | [io.display.resolution] 199 | type = "display" 200 | # Optionally, set which display to base the resolution off 201 | # display_name = "Example" 202 | # And, a percentage of the display 203 | # percentage = 60.5 204 | ``` 205 | 206 | ## Acceleration 207 | 208 | Hardware acceleration in the guest can be manually enabled as follows: 209 | 210 | ```toml 211 | accelerated = true 212 | ``` 213 | 214 | Note that some guest OSes (e.g. macOS) may override this option due to their reliance upon 215 | emulated display devices which do not support hardware acceleration. 216 | 217 | ## Braille 218 | 219 | Quickemu can output braille when this option is set 220 | 221 | ```toml 222 | braille = true 223 | ``` 224 | 225 | # Images 226 | 227 | All paths can be either absolute or relative to the VM directory 228 | 229 | ## ISO/IMG 230 | 231 | ISO and IMG files can be mounted as follows: 232 | 233 | ```toml 234 | [[images.iso]] 235 | path = "ubuntu-24.04.iso" 236 | 237 | [[images.img]] 238 | path = "RecoveryImage.img" 239 | ``` 240 | 241 | Alongside the path, you can add 'always_mount = true' if the image is to be mounted 242 | even after the operating system is installed (determined through disk size). 243 | 244 | ## Disks 245 | 246 | Disk images can be mounted as follows: 247 | 248 | ```toml 249 | path = "disk.qcow2" 250 | # Optional; default dependent on Guest OS 251 | # Size, much like RAM (read above) can be either set as an integer number of bytes 252 | # or a string representing a size with binary-prefix units 253 | size = "30G" 254 | # Optional; defaults to qcow2 255 | format = "raw" 256 | # Optional; defaults to off. NOTE: Requires disk format to be set 257 | preallocation = "full" 258 | ``` 259 | 260 | Disks will be created using qemu-img if they do not already exist. 261 | 262 | # Networking 263 | 264 | All options here must be placed under [network] in your config file. 265 | 266 | ## Disable networking 267 | 268 | ```toml 269 | type = "none" 270 | ``` 271 | 272 | ## NAT 273 | 274 | ```toml 275 | type = "nat" 276 | # Set a desired SSH port. By default, 22220 will be used 277 | ssh_port = 22220 278 | # Restrict networking to only the guest and virtual devices 279 | restrict = true 280 | 281 | # Set each port forward in the array like this 282 | [[network.port_forwards]] 283 | host = 8080 284 | guest = 8080 285 | ``` 286 | 287 | ## Bridged 288 | 289 | ```toml 290 | type = "bridged" 291 | # You must specify a bridge interface 292 | bridge = "br0" 293 | # Optionally specify a mac address. Must be in the range 52:54:00:AB:00:00 - 52:54:00:AB:FF:FF 294 | mac_addr = "52:54:00:AB:51:AE" 295 | ``` 296 | 297 | ## Monitor and Serial 298 | 299 | The QEMU monitor and serial outputs can each be manually configured. 300 | By default, they will use unix sockets with a path in your VM directory. 301 | 302 | ### Socket 303 | 304 | ```toml 305 | [network.serial] 306 | type = "socket" 307 | socketpath = "vm-socket.sock" 308 | ``` 309 | 310 | ### Telnet 311 | 312 | ```toml 313 | [network.monitor] 314 | type = "telnet" 315 | # The Telnet address has a default value unique to each monitor and serial. 316 | # To manually specify, include a full socket address, including both an IP address and port 317 | address = "127.0.0.1:4440" 318 | ``` 319 | 320 | -------------------------------------------------------------------------------- /quickemu/core/src/args/images/disks.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, collections::HashSet, ffi::OsString, path::Path, process::Command}; 2 | 3 | use serde::Deserialize; 4 | use size::Size; 5 | 6 | use crate::{ 7 | arg, 8 | data::{DiskFormat, GuestOS, Images, MacOSRelease, PreAlloc}, 9 | error::Error, 10 | oarg, 11 | utils::{ArgDisplay, EmulatorArgs, QemuArg}, 12 | }; 13 | 14 | const MIN_DISK_SIZE: u64 = 197_632 * 8; 15 | const MAC_BOOTLOADER: [&str; 2] = ["OpenCore.qcow2", "ESP.qcow2"]; 16 | 17 | impl<'a> Images { 18 | pub(crate) fn disk_args(&'a self, guest: GuestOS, vm_dir: &Path, status_quo: bool, used_indices: &mut HashSet) -> Result, Error> { 19 | let mut key = 1; 20 | 21 | let non_disk_keys = match guest { 22 | // ReactOS ISO & Unattended windows installer must be mounted at index 2 23 | GuestOS::ReactOS | GuestOS::Windows => vec![2], 24 | _ => Vec::new(), 25 | }; 26 | 27 | let bootloader = matches!(guest, GuestOS::MacOS { .. }) 28 | .then(|| { 29 | MAC_BOOTLOADER 30 | .iter() 31 | .map(|p| vm_dir.join(p)) 32 | .find(|p| p.exists()) 33 | .ok_or(Error::MacBootloader) 34 | .map(|p| MountedDisk { 35 | path: Cow::Owned(p), 36 | format: DiskFormat::Qcow2 { preallocation: PreAlloc::Off }, 37 | index: 0, 38 | is_new: false, 39 | size: 0, 40 | }) 41 | }) 42 | .transpose()?; 43 | let ahci = matches!(guest, GuestOS::MacOS { .. } | GuestOS::KolibriOS); 44 | 45 | non_disk_keys.iter().for_each(|key| { 46 | used_indices.insert(*key); 47 | }); 48 | 49 | let mut installed = false; 50 | 51 | let mounted_disks = self 52 | .disk 53 | .iter() 54 | .map(|disk| { 55 | let path = if disk.path.is_absolute() { 56 | Cow::Borrowed(disk.path.as_path()) 57 | } else { 58 | Cow::Owned(vm_dir.join(&disk.path)) 59 | }; 60 | Ok(if !path.exists() { 61 | let size = disk.size.unwrap_or(guest.default_disk_size()); 62 | create_disk_image(&path, size, disk.format)?; 63 | MountedDisk::new(path, disk.format, &mut key, used_indices, true, size) 64 | } else { 65 | let QemuImgInfo { actual_size, virtual_size } = find_disk_size(&path)?; 66 | if disk.format.prealloc_enabled() || actual_size >= MIN_DISK_SIZE { 67 | installed = true; 68 | } 69 | MountedDisk::new(path, disk.format, &mut key, used_indices, false, virtual_size) 70 | }) 71 | }) 72 | .collect::>, Error>>()?; 73 | 74 | non_disk_keys.iter().for_each(|key| { 75 | used_indices.remove(key); 76 | }); 77 | 78 | Ok(DiskArgs { 79 | guest, 80 | mounted_disks, 81 | status_quo, 82 | ahci, 83 | bootloader, 84 | installed, 85 | }) 86 | } 87 | } 88 | 89 | fn create_disk_image(path: &Path, size: u64, format: DiskFormat) -> Result<(), Error> { 90 | #[cfg(not(feature = "inbuilt_commands"))] 91 | let mut command = Command::new("qemu-img"); 92 | 93 | command 94 | .arg("create") 95 | .arg("-q") 96 | .arg("-f") 97 | .arg(format.as_ref()) 98 | .arg(path) 99 | .arg(size.to_string()) 100 | .arg("-o") 101 | .arg(format.prealloc_arg()); 102 | 103 | let output = command.output().map_err(|e| Error::Command("qemu-img", e.to_string()))?; 104 | 105 | if !output.status.success() { 106 | return Err(Error::DiskCreationFailed(String::from_utf8_lossy(&output.stderr).to_string())); 107 | } 108 | Ok(()) 109 | } 110 | 111 | #[derive(Deserialize)] 112 | #[serde(rename_all = "kebab-case")] 113 | struct QemuImgInfo { 114 | actual_size: u64, 115 | virtual_size: u64, 116 | } 117 | 118 | fn find_disk_size(path: &Path) -> Result { 119 | #[cfg(not(feature = "inbuilt_commands"))] 120 | let mut command = Command::new("qemu-img"); 121 | 122 | command.arg("info").arg(path).arg("--output=json"); 123 | 124 | let output = command.output().map_err(|e| Error::Command("qemu-img", e.to_string()))?; 125 | 126 | if !output.status.success() { 127 | return Err(Error::DiskInUse(path.display().to_string())); 128 | } 129 | 130 | serde_json::from_slice(&output.stdout).map_err(|e| Error::DeserializeQemuImgInfo(e.to_string())) 131 | } 132 | 133 | pub(crate) struct DiskArgs<'a> { 134 | guest: GuestOS, 135 | mounted_disks: Vec>, 136 | status_quo: bool, 137 | ahci: bool, 138 | bootloader: Option>, 139 | installed: bool, 140 | } 141 | 142 | impl DiskArgs<'_> { 143 | pub(crate) fn installed(&self) -> bool { 144 | self.installed 145 | } 146 | } 147 | 148 | impl EmulatorArgs for DiskArgs<'_> { 149 | fn display(&self) -> impl IntoIterator { 150 | self.mounted_disks.iter().map(MountedDisk::arg_display) 151 | } 152 | fn qemu_args(&self) -> impl IntoIterator { 153 | let mut args = Vec::new(); 154 | if self.ahci { 155 | args.extend([arg!("-device"), arg!("ahci,id=ahci")]); 156 | } 157 | if let Some(bootloader) = &self.bootloader { 158 | args.extend(bootloader.args(self.guest)); 159 | if self.status_quo { 160 | args.push(arg!("-snapshot")); 161 | } 162 | } 163 | 164 | args.into_iter().chain(self.mounted_disks.iter().flat_map(|disk| { 165 | let mut args = disk.args(self.guest); 166 | if self.status_quo { 167 | args.push(arg!("-snapshot")); 168 | } 169 | args 170 | })) 171 | } 172 | } 173 | 174 | struct MountedDisk<'a> { 175 | path: Cow<'a, Path>, 176 | format: DiskFormat, 177 | index: u32, 178 | is_new: bool, 179 | size: u64, 180 | } 181 | 182 | impl<'a> MountedDisk<'a> { 183 | fn args(&self, guest: GuestOS) -> Vec { 184 | let (bus_arg, disk_name) = match guest { 185 | GuestOS::MacOS { release } if release < MacOSRelease::Catalina => ("ide-hd,bus=ahci.2,drive=", "SystemDisk"), 186 | GuestOS::KolibriOS => ("ide-hd,bus=ahci.0,drive=", "SystemDisk"), 187 | GuestOS::ReactOS => return self.reactos_args(), 188 | _ => ("virtio-blk-pci,drive=", "SystemDisk"), 189 | }; 190 | 191 | let disk_name = match self.index { 192 | 0 => "Bootloader", 193 | 1 => disk_name, 194 | _ => &format!("Disk{}", self.index), 195 | }; 196 | let device = bus_arg.to_string() + disk_name; 197 | 198 | let mut drive_arg = OsString::from("id="); 199 | drive_arg.push(disk_name); 200 | drive_arg.push(",if=none,format="); 201 | drive_arg.push(self.format.as_ref()); 202 | drive_arg.push(",file="); 203 | drive_arg.push(self.path.as_ref()); 204 | 205 | vec![arg!("-device"), oarg!(device), arg!("-drive"), oarg!(drive_arg)] 206 | } 207 | 208 | fn reactos_args(&self) -> Vec { 209 | let mut argument = OsString::from("if=ide,index="); 210 | argument.push(self.index.to_string()); 211 | argument.push(",media=disk,file="); 212 | argument.push(self.path.as_ref()); 213 | vec![arg!("-drive"), oarg!(argument)] 214 | } 215 | 216 | fn new(path: Cow<'a, Path>, format: DiskFormat, key: &mut u32, used_indices: &mut HashSet, is_new: bool, size: u64) -> Self { 217 | while !used_indices.insert(*key) { 218 | *key += 1; 219 | } 220 | let disk = MountedDisk { 221 | path, 222 | format, 223 | index: *key, 224 | is_new, 225 | size, 226 | }; 227 | *key += 1; 228 | disk 229 | } 230 | 231 | fn arg_display(&self) -> ArgDisplay { 232 | let name = if self.is_new { "Disk (Created)" } else { "Disk" }; 233 | ArgDisplay { 234 | name: Cow::Borrowed(name), 235 | value: Cow::Owned(format!("{} ({})", self.path.display(), Size::from_bytes(self.size))), 236 | } 237 | } 238 | } 239 | 240 | impl GuestOS { 241 | fn default_disk_size(&self) -> u64 { 242 | let gib = match self { 243 | Self::Windows | Self::WindowsServer => 64, 244 | Self::MacOS { .. } => 96, 245 | Self::ReactOS | Self::KolibriOS => 16, 246 | _ => 32, 247 | }; 248 | gib * size::consts::GiB as u64 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /quickemu/core/src/config.rs: -------------------------------------------------------------------------------- 1 | use crate::data::*; 2 | use serde::{Deserialize, Serialize}; 3 | use std::{ffi::OsString, path::PathBuf}; 4 | 5 | #[cfg(feature = "quickemu")] 6 | use crate::{ 7 | arg, 8 | error::{ConfigError, Error, MonitorError, Warning}, 9 | full_qemu_args, 10 | live_vm::LiveVM, 11 | oarg, qemu_args, 12 | utils::{ArgDisplay, EmulatorArgs, LaunchFn, LaunchFnReturn, QemuArg}, 13 | }; 14 | #[cfg(feature = "quickemu")] 15 | use std::{ 16 | borrow::Cow, 17 | path::Path, 18 | process::{Child, Command}, 19 | thread::JoinHandle, 20 | }; 21 | #[cfg(feature = "quickemu")] 22 | use which::which; 23 | 24 | #[derive(Default, Clone, Debug, Serialize, Deserialize)] 25 | pub struct Config { 26 | #[serde(default, skip_serializing_if = "is_default")] 27 | pub vm_dir: Option, 28 | #[serde(default, skip_serializing_if = "is_default")] 29 | pub vm_name: String, 30 | pub guest: GuestOS, 31 | #[serde(default, skip_serializing_if = "is_default")] 32 | pub machine: Machine, 33 | #[serde(default, skip_serializing_if = "is_default")] 34 | pub images: Images, 35 | #[serde(default, skip_serializing_if = "is_default")] 36 | pub network: Network, 37 | #[serde(default, skip_serializing_if = "is_default")] 38 | pub io: Io, 39 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 40 | pub extra_args: Vec, 41 | } 42 | 43 | #[cfg(feature = "quickemu")] 44 | #[derive(Debug)] 45 | pub struct QemuArgs { 46 | pub qemu_args: Vec, 47 | pub warnings: Vec, 48 | pub after_launch_fns: Vec, 49 | pub before_launch_fns: Vec, 50 | pub display: Vec, 51 | } 52 | 53 | #[cfg(feature = "quickemu")] 54 | #[derive(Debug)] 55 | pub struct LaunchResult { 56 | pub display: Vec, 57 | pub warnings: Vec, 58 | pub threads: Vec>>, 59 | pub children: Vec, 60 | } 61 | 62 | #[cfg(feature = "quickemu")] 63 | #[allow(clippy::large_enum_variant)] 64 | pub enum ParsedVM { 65 | Config(Config), 66 | Live(LiveVM), 67 | } 68 | 69 | #[cfg(feature = "quickemu")] 70 | impl<'a> Config { 71 | pub fn parse(file: &Path) -> Result { 72 | let contents = std::fs::read_to_string(file)?; 73 | let mut conf: Self = toml::from_str(&contents).map_err(ConfigError::Parse)?; 74 | if conf.vm_dir.is_none() { 75 | if conf.vm_name.is_empty() { 76 | let filename = file.file_name().expect("Filename should exist").to_string_lossy(); 77 | let ext_rindex = filename.bytes().rposition(|b| b == b'.').unwrap_or(0); 78 | conf.vm_name = filename[..ext_rindex].to_string(); 79 | } 80 | conf.vm_dir = Some(file.parent().unwrap().join(&conf.vm_name)); 81 | } 82 | Ok(if let Some(live_vm) = LiveVM::find_active(conf.vm_dir.as_ref().unwrap())? { 83 | ParsedVM::Live(live_vm) 84 | } else { 85 | ParsedVM::Config(conf) 86 | }) 87 | } 88 | 89 | fn finalize(&mut self) -> Result<(), Error> { 90 | if self.vm_name.is_empty() { 91 | self.vm_name = self 92 | .vm_dir 93 | .as_ref() 94 | .unwrap() 95 | .file_name() 96 | .expect("Filename should exist") 97 | .to_string_lossy() 98 | .to_string(); 99 | } 100 | self.network.monitor.validate()?; 101 | self.network.serial.validate()?; 102 | #[cfg(unix)] 103 | { 104 | if let MonitorInner::Socket { socketpath } = &mut self.network.monitor { 105 | if socketpath.is_none() { 106 | *socketpath = Some(self.vm_dir.as_ref().unwrap().join(format!("{}-monitor.socket", self.vm_name))); 107 | } 108 | } 109 | if let MonitorInner::Socket { socketpath } = &mut self.network.serial { 110 | if socketpath.is_none() { 111 | *socketpath = Some(self.vm_dir.as_ref().unwrap().join(format!("{}-serial.socket", self.vm_name))); 112 | } 113 | } 114 | } 115 | Ok(()) 116 | } 117 | 118 | pub fn send_monitor_command(&self, command: &str) -> Result { 119 | self.network.monitor.send_cmd(command) 120 | } 121 | 122 | fn create_live_vm(&self) -> (LiveVM, PathBuf) { 123 | let vm_dir = self.vm_dir.as_ref().unwrap(); 124 | let ssh_port = if let NetworkType::Nat { ssh_port, .. } = &self.network.network_type { 125 | *ssh_port.as_ref() 126 | } else { 127 | None 128 | }; 129 | #[cfg(not(target_os = "macos"))] 130 | let spice_port = if let DisplayType::Spice { spice_port, .. } = self.io.display.display_type { 131 | Some(spice_port) 132 | } else { 133 | None 134 | }; 135 | LiveVM::new( 136 | vm_dir, 137 | ssh_port, 138 | #[cfg(not(target_os = "macos"))] 139 | spice_port, 140 | self.network.monitor.clone(), 141 | self.network.serial.clone(), 142 | ) 143 | } 144 | 145 | pub fn launch(self) -> Result { 146 | let (live_vm, live_vm_file) = self.create_live_vm(); 147 | let qemu_bin_str = match self.machine.arch { 148 | Arch::X86_64 { .. } => "qemu-system-x86_64", 149 | Arch::AArch64 { .. } => "qemu-system-aarch64", 150 | Arch::Riscv64 { .. } => "qemu-system-riscv64", 151 | }; 152 | let qemu_bin = which(qemu_bin_str).map_err(|_| Error::QemuNotFound(qemu_bin_str))?; 153 | let mut qemu_args = self.to_full_qemu_args()?; 154 | 155 | let mut threads = Vec::new(); 156 | let mut children = Vec::new(); 157 | 158 | for launch_fn in qemu_args.before_launch_fns { 159 | for launch_fn_return in launch_fn.call()? { 160 | match launch_fn_return { 161 | LaunchFnReturn::Arg(arg) => qemu_args.qemu_args.push(arg), 162 | LaunchFnReturn::Display(display) => qemu_args.display.push(display), 163 | LaunchFnReturn::Thread(thread) => threads.push(thread), 164 | LaunchFnReturn::Process(child) => children.push(child), 165 | } 166 | } 167 | } 168 | 169 | log::debug!("Launching QEMU with args {:#?}", qemu_args.qemu_args); 170 | 171 | let qemu_process = Command::new(qemu_bin) 172 | .args(qemu_args.qemu_args) 173 | .spawn() 174 | .map_err(|e| Error::Command(qemu_bin_str, e.to_string()))?; 175 | 176 | live_vm.serialize(&live_vm_file, qemu_process.id())?; 177 | 178 | qemu_args.display.push(ArgDisplay { 179 | name: Cow::Borrowed("PID"), 180 | value: Cow::Owned(qemu_process.id().to_string()), 181 | }); 182 | 183 | children.push(qemu_process); 184 | 185 | for launch_fn in qemu_args.after_launch_fns { 186 | for launch_fn_return in launch_fn.call()? { 187 | match launch_fn_return { 188 | LaunchFnReturn::Arg(_) => panic!("Arguments should not be returned in 'after' launch fns"), 189 | LaunchFnReturn::Display(display) => qemu_args.display.push(display), 190 | LaunchFnReturn::Thread(thread) => threads.push(thread), 191 | LaunchFnReturn::Process(child) => children.push(child), 192 | } 193 | } 194 | } 195 | 196 | Ok(LaunchResult { 197 | display: qemu_args.display, 198 | warnings: qemu_args.warnings, 199 | threads, 200 | children, 201 | }) 202 | } 203 | 204 | pub fn to_full_qemu_args(mut self) -> Result { 205 | self.finalize()?; 206 | let vm_dir = self.vm_dir.as_ref().unwrap(); 207 | #[cfg(target_arch = "x86_64")] 208 | self.guest.validate_cpu()?; 209 | 210 | let mut args = full_qemu_args!( 211 | self.basic_args(), 212 | self.machine.args(self.guest, vm_dir, &self.vm_name), 213 | self.io.args(self.machine.arch, self.guest, &self.vm_name), 214 | self.network.args(self.guest, &self.vm_name, self.io.public_dir()), 215 | self.images 216 | .args(self.guest, vm_dir, self.machine.status_quo, self.network.monitor), 217 | )?; 218 | 219 | args.qemu_args.extend(self.extra_args.into_iter().map(|arg| oarg!(arg))); 220 | Ok(args) 221 | } 222 | 223 | pub fn to_qemu_args(mut self) -> Result<(Vec, Vec), Error> { 224 | self.finalize()?; 225 | let vm_dir = self.vm_dir.as_ref().unwrap(); 226 | #[cfg(target_arch = "x86_64")] 227 | self.guest.validate_cpu()?; 228 | 229 | let (mut args, warnings) = qemu_args!( 230 | self.basic_args(), 231 | self.machine.args(self.guest, vm_dir, &self.vm_name), 232 | self.io.args(self.machine.arch, self.guest, &self.vm_name), 233 | self.network.args(self.guest, &self.vm_name, self.io.public_dir()), 234 | self.images 235 | .args(self.guest, vm_dir, self.machine.status_quo, self.network.monitor), 236 | )?; 237 | args.extend(self.extra_args.into_iter().map(|arg| oarg!(arg))); 238 | 239 | Ok((args, warnings)) 240 | } 241 | 242 | fn basic_args(&'a self) -> Result<(BasicArgs<'a>, Option), Error> { 243 | Ok(( 244 | BasicArgs { 245 | slew_driftfix: matches!(self.machine.arch, Arch::X86_64 { .. }), 246 | pid_path: self.vm_dir.as_ref().unwrap().join(format!("{}.pid", self.vm_name)), 247 | vm_name: &self.vm_name, 248 | }, 249 | None, 250 | )) 251 | } 252 | } 253 | 254 | #[cfg(feature = "quickemu")] 255 | pub(crate) struct BasicArgs<'a> { 256 | slew_driftfix: bool, 257 | pid_path: PathBuf, 258 | vm_name: &'a str, 259 | } 260 | 261 | #[cfg(feature = "quickemu")] 262 | impl EmulatorArgs for BasicArgs<'_> { 263 | fn qemu_args(&self) -> impl IntoIterator { 264 | let mut args = Vec::with_capacity(4); 265 | 266 | let rtc = if self.slew_driftfix { 267 | "base=localtime,clock=host,driftfix=slew" 268 | } else { 269 | "base=localtime,clock=host" 270 | }; 271 | 272 | args.push(arg!("-rtc")); 273 | args.push(arg!(rtc)); 274 | 275 | args.push(arg!("-pidfile")); 276 | args.push(oarg!(self.pid_path.clone())); 277 | 278 | #[cfg(target_os = "linux")] 279 | { 280 | args.push(arg!("-name")); 281 | args.push(oarg!(format!("{},process={}", self.vm_name, self.vm_name))); 282 | } 283 | args 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /quickget/core/src/config_search.rs: -------------------------------------------------------------------------------- 1 | use quickemu_core::data::Arch; 2 | 3 | use crate::{ 4 | data_structures::{Config, OS}, 5 | error::ConfigSearchError, 6 | }; 7 | use std::{ 8 | fs::File, 9 | io::Write, 10 | path::{Path, PathBuf}, 11 | time::{SystemTime, UNIX_EPOCH}, 12 | }; 13 | 14 | const CONFIG_URL: &str = "https://github.com/lj3954/quickget_cigo/releases/download/daily/quickget_data.json.zst"; 15 | 16 | #[derive(Debug, Default)] 17 | pub struct ConfigSearch { 18 | configs: Vec, 19 | cache_file: Option, 20 | chosen_os: Option, 21 | release_is_chosen: bool, 22 | edition_is_chosen: bool, 23 | } 24 | 25 | impl ConfigSearch { 26 | pub async fn new() -> Result { 27 | let cache_dir = dirs::cache_dir().ok_or(ConfigSearchError::FailedCacheDir)?; 28 | Self::new_full(cache_dir, false).await 29 | } 30 | pub async fn new_refreshed() -> Result { 31 | let cache_dir = dirs::cache_dir().ok_or(ConfigSearchError::FailedCacheDir)?; 32 | Self::new_full(cache_dir, true).await 33 | } 34 | #[deprecated(since = "1.2.0", note = "use new_full to specify details instead")] 35 | pub async fn new_with_cache_dir(cache_dir: PathBuf) -> Result { 36 | Self::new_full(cache_dir, false).await 37 | } 38 | pub async fn new_full(cache_dir: PathBuf, refresh: bool) -> Result { 39 | if !cache_dir.exists() { 40 | return Err(ConfigSearchError::InvalidCacheDir(cache_dir)); 41 | } 42 | let cache_file_path = cache_dir.join("quickget_data.json.zst"); 43 | 44 | let (configs, cache_file) = if !refresh && cache_file_path.is_valid()? { 45 | if let Some(data) = File::open(&cache_file_path) 46 | .ok() 47 | .and_then(|cache_file| Some((read_cache_file(&cache_file).ok()?, cache_file))) 48 | { 49 | data 50 | } else { 51 | get_new_cache_data(&cache_file_path).await? 52 | } 53 | } else { 54 | get_new_cache_data(&cache_file_path).await? 55 | }; 56 | 57 | Ok(Self { 58 | configs, 59 | cache_file: Some(cache_file), 60 | ..Default::default() 61 | }) 62 | } 63 | pub async fn new_without_cache() -> Result { 64 | gather_configs(None).await.map(|configs| Self { 65 | configs, 66 | cache_file: None, 67 | ..Default::default() 68 | }) 69 | } 70 | pub async fn refresh_data(&mut self) -> Result<(), ConfigSearchError> { 71 | self.configs = gather_configs(self.cache_file.as_mut()).await?; 72 | Ok(()) 73 | } 74 | pub fn get_os_list(&self) -> &[OS] { 75 | &self.configs 76 | } 77 | pub fn into_os_list(self) -> Vec { 78 | self.configs 79 | } 80 | pub fn get_chosen_os(&self) -> Option<&OS> { 81 | self.chosen_os.as_ref() 82 | } 83 | pub fn get_configs(&self) -> Result<&[Config], ConfigSearchError> { 84 | let os = self.chosen_os.as_ref().ok_or(ConfigSearchError::RequiredOS)?; 85 | Ok(&os.releases) 86 | } 87 | pub fn list_os_names(&self) -> Vec<&str> { 88 | self.configs.iter().map(|OS { name, .. }| &**name).collect() 89 | } 90 | pub fn filter_os(&mut self, os: &str) -> Result<&mut OS, ConfigSearchError> { 91 | let os = self 92 | .configs 93 | .drain(..) 94 | .find(|OS { name, .. }| name == os) 95 | .ok_or(ConfigSearchError::InvalidOS(os.into()))?; 96 | 97 | self.chosen_os = Some(os); 98 | Ok(self.chosen_os.as_mut().unwrap()) 99 | } 100 | pub fn list_architectures(&self) -> Result, ConfigSearchError> { 101 | let os = self.chosen_os.as_ref().ok_or(ConfigSearchError::RequiredOS)?; 102 | 103 | let architectures = Arch::iter() 104 | .filter(|search_arch| os.releases.iter().any(|Config { arch, .. }| arch == search_arch)) 105 | .collect::>(); 106 | 107 | Ok(architectures) 108 | } 109 | pub fn filter_arch_supported_os(&mut self, matching_arch: &Arch) -> Result<(), ConfigSearchError> { 110 | self.configs 111 | .retain(|OS { releases, .. }| releases.iter().any(|Config { arch, .. }| arch == matching_arch)); 112 | 113 | if self.configs.is_empty() { 114 | return Err(ConfigSearchError::InvalidArchitecture(matching_arch.to_owned())); 115 | } 116 | 117 | Ok(()) 118 | } 119 | pub fn filter_arch_configs(&mut self, matching_arch: &Arch) -> Result<(), ConfigSearchError> { 120 | let os = self.chosen_os.as_mut().ok_or(ConfigSearchError::RequiredOS)?; 121 | os.filter_arch(matching_arch) 122 | } 123 | pub fn list_releases(&self) -> Result, ConfigSearchError> { 124 | let os = self.chosen_os.as_ref().ok_or(ConfigSearchError::RequiredOS)?; 125 | let mut releases = os 126 | .releases 127 | .iter() 128 | .map(|Config { release, .. }| release.as_str()) 129 | .collect::>(); 130 | 131 | releases.sort_unstable(); 132 | releases.dedup(); 133 | 134 | Ok(releases) 135 | } 136 | pub fn filter_release(&mut self, matching_release: &str) -> Result<(), ConfigSearchError> { 137 | let os = self.chosen_os.as_mut().ok_or(ConfigSearchError::RequiredOS)?; 138 | self.release_is_chosen = true; 139 | os.filter_release(matching_release) 140 | } 141 | pub fn list_editions(&mut self) -> Result>, ConfigSearchError> { 142 | let os = self.chosen_os.as_ref().ok_or(ConfigSearchError::RequiredOS)?; 143 | if !self.release_is_chosen { 144 | return Err(ConfigSearchError::RequiredRelease); 145 | } 146 | let mut editions = os 147 | .releases 148 | .iter() 149 | .filter_map(|Config { edition, .. }| edition.as_deref()) 150 | .collect::>(); 151 | 152 | editions.sort_unstable(); 153 | editions.dedup(); 154 | 155 | if editions.is_empty() { 156 | self.edition_is_chosen = true; 157 | Ok(None) 158 | } else { 159 | Ok(Some(editions)) 160 | } 161 | } 162 | pub fn filter_edition(&mut self, matching_edition: &str) -> Result<(), ConfigSearchError> { 163 | let os = self.chosen_os.as_mut().ok_or(ConfigSearchError::RequiredOS)?; 164 | if !self.release_is_chosen { 165 | return Err(ConfigSearchError::RequiredRelease); 166 | } else if self.edition_is_chosen { 167 | return Err(ConfigSearchError::NoEditions); 168 | } 169 | self.edition_is_chosen = true; 170 | os.filter_edition(matching_edition) 171 | } 172 | pub fn pick_best_match(self) -> Result { 173 | let mut os = self.chosen_os.ok_or(ConfigSearchError::RequiredOS)?; 174 | if !self.release_is_chosen { 175 | return Err(ConfigSearchError::RequiredRelease); 176 | } else if !self.edition_is_chosen { 177 | return Err(ConfigSearchError::RequiredEdition); 178 | } 179 | 180 | let preferred_arch = || match std::env::consts::ARCH { 181 | "aarch64" => Arch::AArch64 { machine: Default::default() }, 182 | "riscv64" => Arch::Riscv64 { machine: Default::default() }, 183 | _ => Arch::X86_64 { machine: Default::default() }, 184 | }; 185 | 186 | let config = if os.releases.len() == 1 { 187 | os.releases.pop().unwrap() 188 | } else if let Some(position) = os.releases.iter().position(|Config { arch, .. }| arch == &preferred_arch()) { 189 | os.releases.remove(position) 190 | } else { 191 | os.releases.pop().unwrap() 192 | }; 193 | 194 | Ok(QuickgetConfig { os: os.name, config }) 195 | } 196 | } 197 | 198 | #[derive(Debug)] 199 | pub struct QuickgetConfig { 200 | pub os: String, 201 | pub config: Config, 202 | } 203 | 204 | fn read_cache_file(file: &File) -> Result, ConfigSearchError> { 205 | let reader = zstd::stream::Decoder::new(file)?; 206 | serde_json::from_reader(reader).map_err(ConfigSearchError::from) 207 | } 208 | 209 | async fn gather_configs(file: Option<&mut File>) -> Result, ConfigSearchError> { 210 | let request = reqwest::get(CONFIG_URL).await?; 211 | let data = request.bytes().await?; 212 | if let Some(file) = file { 213 | file.write_all(&data)?; 214 | } 215 | let reader = zstd::stream::Decoder::new(&data[..])?; 216 | serde_json::from_reader(reader).map_err(ConfigSearchError::from) 217 | } 218 | 219 | trait IsValid { 220 | fn is_valid(&self) -> Result; 221 | } 222 | 223 | impl IsValid for PathBuf { 224 | fn is_valid(&self) -> Result { 225 | if self.exists() { 226 | if let Ok(metadata) = self.metadata() { 227 | if let Ok(modified) = metadata.modified() { 228 | let modified_date = modified.duration_since(UNIX_EPOCH)?.as_secs() / 86400; 229 | let date_today = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() / 86400; 230 | return Ok(metadata.is_file() && modified_date == date_today); 231 | } 232 | } 233 | } 234 | Ok(false) 235 | } 236 | } 237 | 238 | async fn get_new_cache_data(cache_file_path: &Path) -> Result<(Vec, File), ConfigSearchError> { 239 | let mut cache_file = File::create(cache_file_path)?; 240 | Ok((gather_configs(Some(&mut cache_file)).await?, cache_file)) 241 | } 242 | 243 | impl OS { 244 | pub fn filter_release(&mut self, matching_release: &str) -> Result<(), ConfigSearchError> { 245 | self.releases.retain(|Config { release, .. }| release == matching_release); 246 | 247 | if self.releases.is_empty() { 248 | return Err(ConfigSearchError::InvalidRelease( 249 | matching_release.into(), 250 | self.pretty_name.clone(), 251 | )); 252 | } 253 | Ok(()) 254 | } 255 | pub fn filter_edition(&mut self, matching_edition: &str) -> Result<(), ConfigSearchError> { 256 | self.releases 257 | .retain(|Config { edition, .. }| edition.as_ref().is_none_or(|edition| edition == matching_edition)); 258 | 259 | if self.releases.is_empty() { 260 | return Err(ConfigSearchError::InvalidEdition(matching_edition.into())); 261 | } 262 | Ok(()) 263 | } 264 | pub fn filter_arch(&mut self, matching_arch: &Arch) -> Result<(), ConfigSearchError> { 265 | self.releases.retain(|Config { arch, .. }| arch == matching_arch); 266 | 267 | if self.releases.is_empty() { 268 | return Err(ConfigSearchError::InvalidArchitecture(matching_arch.to_owned())); 269 | } 270 | Ok(()) 271 | } 272 | } 273 | --------------------------------------------------------------------------------