extern crate windows_bindgen;
fn main() -> Result<()> {
#[cfg(all(not(doc), feature = "nuget"))]
match webview2_nuget::install() {
Ok(package_root) => {
webview2_nuget::update_libs(&package_root)?;
webview2_nuget::update_callback_interfaces(&package_root)?;
let mut lib_path = package_root;
lib_path.push("build");
lib_path.push("native");
webview2_link::update_rustc_flags(lib_path)?;
}
Err(e) => {
panic!("{}", e.to_string());
}
}
#[cfg(any(doc, not(feature = "nuget")))]
webview2_link::update_rustc_flags(webview2_link::output_libs(
webview2_path::get_manifest_dir()?,
webview2_path::get_out_dir()?,
)?)?;
webview2_bindgen::update_bindings()?;
Ok(())
}
#[derive(Debug, Error)]
pub enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Var(#[from] std::env::VarError),
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error(transparent)]
Regex(#[from] regex::Error),
#[error("Missing Version")]
MissingVersion,
#[error("Missing Typedef")]
MissingTypedef,
#[error("Missing Path")]
MissingPath(std::path::PathBuf),
#[error("Failed to run nuget CLI.\n{0}")]
NugetCli(String),
#[cfg(not(windows))]
#[error("The nuget CLI requires mono to run on this host.")]
MissingMono,
}
pub type Result<T> = std::result::Result<T, Error>;
#[macro_use]
extern crate thiserror;
mod webview2_path {
use std::{convert::From, env, path::PathBuf};
pub fn get_out_dir() -> super::Result<PathBuf> {
Ok(PathBuf::from(env::var("OUT_DIR")?))
}
pub fn get_manifest_dir() -> super::Result<PathBuf> {
Ok(PathBuf::from(env::var("CARGO_MANIFEST_DIR")?))
}
}
#[cfg(all(not(doc), feature = "nuget"))]
mod webview2_nuget {
use std::{
convert::From,
fs,
io::{Read, Write},
path::{Path, PathBuf},
process::{Command, Output},
};
use regex::Regex;
use super::webview2_path::*;
include!("./src/callback_interfaces.rs");
const WEBVIEW2_NAME: &str = "Microsoft.Web.WebView2";
const WEBVIEW2_VERSION: &str = "1.0.1774.30";
pub fn install() -> super::Result<PathBuf> {
let out_dir = get_out_dir()?;
let install_root = match out_dir.to_str() {
Some(path) => path,
None => return Err(super::Error::MissingPath(out_dir)),
};
let mut package_root = out_dir.clone();
package_root.push(format!("{WEBVIEW2_NAME}.{WEBVIEW2_VERSION}"));
if !check_nuget_dir(install_root)? {
let mut nuget_path = get_manifest_dir()?;
nuget_path.push("tools");
nuget_path.push("nuget.exe");
let nuget_tool = match nuget_path.to_str() {
Some(path) => path,
None => return Err(super::Error::MissingPath(nuget_path)),
};
let output = install_sdk_package(nuget_tool, install_root)?;
if !output.status.success() {
return Err(super::Error::NugetCli(format!(
"{}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
)));
}
if !check_nuget_dir(install_root)? {
return Err(super::Error::MissingPath(package_root));
}
}
Ok(package_root)
}
fn check_nuget_dir(install_root: &str) -> super::Result<bool> {
let nuget_path = format!("{WEBVIEW2_NAME}.{WEBVIEW2_VERSION}");
let mut dir_iter = fs::read_dir(install_root)?.filter(|dir| match dir {
Ok(dir) => match dir.file_type() {
Ok(file_type) => {
file_type.is_dir()
&& match dir.file_name().to_str() {
Some(name) => name.eq_ignore_ascii_case(&nuget_path),
None => false,
}
}
Err(_) => false,
},
Err(_) => false,
});
Ok(dir_iter.next().is_some())
}
#[cfg(not(windows))]
fn install_sdk_package(nuget_tool: &str, install_root: &str) -> super::Result<Output> {
Command::new("mono")
.args(&[
nuget_tool,
"install",
WEBVIEW2_NAME,
"-OutputDirectory",
install_root,
"-Version",
WEBVIEW2_VERSION,
"-Source",
"https://api.nuget.org/v3/index.json",
])
.output()
.map_err(|_| super::Error::MissingMono)
}
#[cfg(windows)]
fn install_sdk_package(nuget_tool: &str, install_root: &str) -> super::Result<Output> {
Command::new(nuget_tool)
.args([
"install",
WEBVIEW2_NAME,
"-OutputDirectory",
install_root,
"-Version",
WEBVIEW2_VERSION,
"-Source",
"https://api.nuget.org/v3/index.json",
])
.output()
.map_err(|_| super::Error::NugetCli(String::from("nuget.exe not found")))
}
pub fn update_libs(package_root: &Path) -> super::Result<()> {
const WEBVIEW2_LIBS: &[&str] = &[
"WebView2Loader.dll",
"WebView2Loader.dll.lib",
"WebView2LoaderStatic.lib",
];
const WEBVIEW2_TARGETS: &[&str] = &["arm64", "x64", "x86"];
let mut native_dir = package_root.to_path_buf();
native_dir.push("build");
native_dir.push("native");
let manifest_dir = get_manifest_dir()?;
for target in WEBVIEW2_TARGETS {
for lib in WEBVIEW2_LIBS {
let mut lib_src = native_dir.clone();
lib_src.push(target);
lib_src.push(lib);
let mut lib_dest = manifest_dir.clone();
lib_dest.push(target);
if !lib_dest.is_dir() {
fs::create_dir(lib_dest.as_path())?;
}
lib_dest.push(lib);
eprintln!("Copy from {lib_src:?} -> {lib_dest:?}");
fs::copy(lib_src.as_path(), lib_dest.as_path())?;
}
}
Ok(())
}
pub fn update_callback_interfaces(package_root: &Path) -> super::Result<bool> {
let interfaces = get_callback_interfaces(package_root)?;
let declared = all_declared().into_iter().map(String::from).collect();
if interfaces == declared {
return Ok(false);
}
let mut source_path = get_manifest_dir()?;
source_path.push("src");
source_path.push("callback_interfaces.rs");
let mut source_file = fs::File::create(source_path)?;
writeln!(
source_file,
r#"use std::collections::BTreeSet;
/// Generate a list of all `ICoreWebView2...Handler` interfaces declared in `WebView2.h`. This is
/// for testing purposes to make sure they are all covered in [callback.rs](../../src/callback.rs).
pub fn all_declared() -> BTreeSet<&'static str> {{
let mut interfaces = BTreeSet::new();
"#,
)?;
for interface in interfaces {
writeln!(source_file, r#" interfaces.insert("{interface}");"#)?;
}
writeln!(
source_file,
r#"
interfaces
}}"#
)?;
Ok(true)
}
fn get_callback_interfaces(package_root: &Path) -> super::Result<BTreeSet<String>> {
let mut include_path = package_root.to_path_buf();
include_path.push("build");
include_path.push("native");
include_path.push("include");
include_path.push("WebView2.h");
let mut contents = String::new();
fs::File::open(include_path)?.read_to_string(&mut contents)?;
let pattern =
Regex::new(r#"^\s*typedef\s+interface\s+(ICoreWebView2[A-Za-z0-9]+Handler)\s+"#)?;
let interfaces: BTreeSet<String> = contents
.lines()
.filter_map(|line| pattern.captures(line))
.filter_map(|captures| captures.get(1))
.map(|match_1| String::from(match_1.as_str()))
.collect();
if interfaces.is_empty() {
Err(super::Error::MissingTypedef)
} else {
Ok(interfaces)
}
}
}
mod webview2_link {
use std::{env, path::PathBuf};
#[cfg(any(doc, not(feature = "nuget")))]
pub fn output_libs(manifest_dir: PathBuf, out_dir: PathBuf) -> super::Result<PathBuf> {
const WEBVIEW2_LIBS: &[&str] = &[
"WebView2Loader.dll",
"WebView2Loader.dll.lib",
"WebView2LoaderStatic.lib",
];
const WEBVIEW2_TARGETS: &[&str] = &["arm64", "x64", "x86"];
for target in WEBVIEW2_TARGETS {
for lib in WEBVIEW2_LIBS {
use std::fs;
let mut lib_src = manifest_dir.clone();
lib_src.push(target);
lib_src.push(lib);
let mut lib_dest = out_dir.clone();
lib_dest.push(target);
if !lib_dest.is_dir() {
fs::create_dir(lib_dest.as_path())?;
}
lib_dest.push(lib);
eprintln!("Copy from {:?} -> {:?}", lib_src, lib_dest);
fs::copy(lib_src.as_path(), lib_dest.as_path())?;
}
}
Ok(out_dir)
}
pub fn update_rustc_flags(lib_path: PathBuf) -> super::Result<()> {
let target_arch = match env::var("CARGO_CFG_TARGET_ARCH")?.as_str() {
"x86_64" => "x64",
"x86" => "x86",
"arm" => "arm",
"aarch64" => "arm64",
unimplemented => unimplemented!(
"`{}` architecture set by `CARGO_CFG_TARGET_ARCH`",
unimplemented
),
};
let mut lib_path = lib_path;
lib_path.push(target_arch);
match lib_path.to_str() {
Some(path) if lib_path.exists() => println!("cargo:rustc-link-search=native={path}"),
_ => unimplemented!("`{}` is not supported by WebView2", target_arch),
};
Ok(())
}
}
mod webview2_bindgen {
use std::{
fs,
io::{Read, Write},
path::{Path, PathBuf},
};
use regex::Regex;
use windows_bindgen::{namespace, namespace_impl, Gen};
use windows_metadata::reader::{File, Reader};
use super::webview2_path::*;
pub fn update_bindings() -> super::Result<bool> {
let source_path = generate_bindings()?;
format_bindings(&source_path)?;
let source = read_bindings(&source_path)?;
let mut dest_path = get_manifest_dir()?;
dest_path.push("src");
dest_path.push("Microsoft");
dest_path.push("Web");
dest_path.push("WebView2");
dest_path.push("Win32");
dest_path.push("mod.rs");
let dest = read_bindings(&dest_path)?;
if source != dest {
fs::copy(&source_path, &dest_path)?;
Ok(true)
} else {
Ok(false)
}
}
fn generate_bindings() -> super::Result<PathBuf> {
const WINMD_FILE: &str = "Microsoft.Web.WebView2.Win32.winmd";
let mut winmd_path = get_manifest_dir()?;
winmd_path.push("winmd");
winmd_path.push(WINMD_FILE);
let winmd_files: Vec<_> =
File::with_default(&[winmd_path.to_str().expect("invalid winmd path")])
.expect("failed to load winmd");
let metadata_reader = Reader::new(&winmd_files);
let tree = metadata_reader.tree("Microsoft.Web.WebView2.Win32", &Default::default());
let mut gen = Gen::new(&metadata_reader);
gen.namespace = tree.namespace;
let mut source_path = get_out_dir()?;
source_path.push("mod.rs");
let mut source_file = fs::File::create(source_path.clone())?;
source_file.write_all(patch_bindings(namespace(&gen, &tree))?.as_bytes())?;
source_file.write_all(namespace_impl(&gen, &tree).as_bytes())?;
Ok(source_path)
}
fn patch_bindings(bindings: String) -> super::Result<String> {
let pattern = Regex::new(
r#"#\s*\[\s*link\s*\(\s*name\s*=\s*"WebView2LoaderStatic"\s*,\s*kind\s*=\s*"static"\s*\)\s*\]"#,
)?;
let replacement = r#"
#[cfg_attr(target_env = "msvc", link(name = "WebView2LoaderStatic", kind = "static"))]
#[cfg_attr(not(target_env = "msvc"), link(name = "WebView2Loader"))]
"#;
Ok(pattern
.replace_all(&bindings, replacement)
.replace(r#"extern"system""#, r#"extern "system""#))
}
fn format_bindings(source_path: &Path) -> super::Result<()> {
let mut cmd = ::std::process::Command::new("rustfmt");
cmd.arg(source_path);
cmd.output()?;
Ok(())
}
fn read_bindings(source_path: &Path) -> super::Result<String> {
let mut source_file = fs::File::open(source_path)?;
let mut updated = String::default();
source_file.read_to_string(&mut updated)?;
Ok(updated)
}
}