#!/usr/bin/env -S cargo +nightly-gnu -Z script
---
[package]
edition = "2024"
[dependencies]
anyhow = "*"
bindgen = "*"
cargo_metadata = "*"
reqwest = { version = "*", features = ["blocking"] }
syn = "*"
zip = "*"
[lints.rust]
non_snake_case = "allow"
---
use anyhow::{anyhow, Result};
use bindgen::{
builder, AliasVariation, EnumVariation, FieldVisibilityKind, MacroTypeVariation,
NonCopyUnionStyle,
};
use cargo_metadata::MetadataCommand;
use reqwest::blocking::get;
use std::{
fs::{create_dir_all, read_to_string, write, File, OpenOptions},
io::copy,
path::{Path, PathBuf},
};
use syn::{parse_str, File as RustFile, ForeignItem, ForeignItemFn, Item, ItemForeignMod};
use zip::ZipArchive;
const QEMU_GITHUB_URL_BASE: &str = "https://github.com/qemu/qemu/";
const QEMU_VERSIONS: &[&str] = &[
"b0ca999a43a22b38158a222233d3f5881648bb4f",
"1332b8dd434674480f0feb2cdf3bbaebb85b4240",
"c25df57ae8f9fe1c72eee2dab37d76d904ac382e",
"7de77d37880d7267a491cb32a1b2232017d1e545",
"595cd9ce2ec9330882c991a647d5bc2a5640f380",
"f8b2f64e2336a28bf0d50b6ef8a7d8c013e9bcf3",
];
fn qemu_git_url(hash: &str) -> String {
format!("{}/archive/{}.zip", QEMU_GITHUB_URL_BASE, hash)
}
fn download(url: &str, destination: &Path) -> Result<()> {
let mut response = get(url)?;
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(destination)?;
response.copy_to(&mut file)?;
Ok(())
}
fn extract_zip(archive: &Path, destination: &Path) -> Result<()> {
let archive = File::open(archive)?;
let mut archive = ZipArchive::new(archive)?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let file_path = file.mangled_name();
let components: Vec<_> = file_path.components().collect();
if components.len() <= 1 {
continue;
}
let new_path = components[1..].iter().collect::<PathBuf>();
let out_path = destination.join(new_path);
if file.is_dir() {
create_dir_all(&out_path)?;
} else {
if let Some(parent) = out_path.parent() {
create_dir_all(parent)?;
}
let mut out_file = File::create(&out_path)?;
copy(&mut file, &mut out_file)?;
}
}
Ok(())
}
fn generate_bindings(
qemu_plugin_header: &Path,
bindings_path: &Path,
def_path: &Path,
) -> Result<()> {
let header_contents = read_to_string(qemu_plugin_header)?;
let header_file_name = qemu_plugin_header
.file_name()
.ok_or_else(|| anyhow!("Failed to get file name"))?
.to_str()
.ok_or_else(|| anyhow!("Failed to convert file name to string"))?;
let header_contents = header_contents.replace("#include <glib.h>", "");
let header_contents = format!(
"#include <stddef.h>\n{}\n{}\n{}\n",
"typedef struct GArray { char *data; unsigned int len; } GArray;",
"typedef struct GByteArray { unsigned char *data; unsigned int len; } GByteArray;",
header_contents,
);
let rust_bindings = builder()
.clang_arg("-fretain-comments-from-system-headers")
.clang_arg("-fparse-all-comments")
.clang_arg("-Wno-everything")
.default_visibility(FieldVisibilityKind::Public)
.default_alias_style(AliasVariation::TypeAlias)
.default_enum_style(EnumVariation::Rust {
non_exhaustive: false,
})
.default_macro_constant_type(MacroTypeVariation::Unsigned)
.default_non_copy_union_style(NonCopyUnionStyle::BindgenWrapper)
.derive_default(true)
.derive_hash(true)
.derive_partialord(true)
.derive_ord(true)
.derive_eq(true)
.derive_partialeq(true)
.generate_comments(true)
.layout_tests(false)
.header_contents(header_file_name, &header_contents)
.blocklist_function("qemu_plugin_install")
.blocklist_item("qemu_plugin_version")
.allowlist_item("qemu_plugin.*")
.allowlist_item("QEMU_PLUGIN.*")
.allowlist_item("G.*")
.allowlist_item("g_.*")
.generate()?;
rust_bindings.write_to_file(bindings_path)?;
let parsed: RustFile = parse_str(&rust_bindings.to_string())?;
let mut export_names: Vec<String> = parsed
.items
.iter()
.filter_map(|item| {
if let Item::ForeignMod(ItemForeignMod { items, .. }) = item {
Some(items)
} else {
None
}
})
.flat_map(|items| {
items.iter().filter_map(|item| {
if let ForeignItem::Fn(ForeignItemFn { sig, .. }) = item {
Some(sig.ident.to_string())
} else {
None
}
})
})
.collect();
export_names.sort();
let mut output = String::from("EXPORTS\n");
output.extend(export_names.into_iter().map(|name| format!(" {}\n", name)));
write(&def_path, output)?;
Ok(())
}
fn generate(tmp_dir: &Path, out_dir: &Path, version: usize) -> Result<()> {
println!(
"Generating bindings with tmp={:?} out={:?} version={}",
tmp_dir, out_dir, version
);
let src_archive = tmp_dir.join(format!("qemu-{}.zip", QEMU_VERSIONS[version]));
let src_dir = tmp_dir.join(format!("qemu-{}", QEMU_VERSIONS[version]));
if !src_archive.exists() {
let qemu_url = qemu_git_url(QEMU_VERSIONS[version]);
println!("Downloading {} to {:?}", qemu_url, src_archive);
download(&qemu_url, &src_archive)?;
}
if !src_dir.exists() {
println!("Extracting {:?} to {:?}", src_archive, src_dir);
extract_zip(&src_archive, &src_dir)?;
}
generate_bindings(
&src_dir.join("include").join("qemu").join("qemu-plugin.h"),
&out_dir.join(&format!("bindings_v{}.rs", version)),
&out_dir.join(&format!("qemu_plugin_api_v{}.def", version)),
)?;
Ok(())
}
fn main() -> Result<()> {
let metadata = MetadataCommand::new().no_deps().exec()?;
let search_package = "qemu-plugin-sys".parse()?;
let package = metadata
.packages
.iter()
.find(|p| p.name == search_package)
.ok_or_else(|| anyhow!("Failed to find package"))?;
let out_dir = package
.manifest_path
.parent()
.ok_or_else(|| anyhow!("Failed to get manifest path"))?
.join("src")
.into_std_path_buf();
let tmp_dir = metadata.target_directory.join("tmp").into_std_path_buf();
if !tmp_dir.exists() {
create_dir_all(&tmp_dir)?;
}
generate(&tmp_dir, &out_dir, 0)?;
generate(&tmp_dir, &out_dir, 1)?;
generate(&tmp_dir, &out_dir, 2)?;
generate(&tmp_dir, &out_dir, 3)?;
generate(&tmp_dir, &out_dir, 4)?;
generate(&tmp_dir, &out_dir, 5)?;
Ok(())
}