use krates::semver::Version;
use anyhow::{anyhow, Result};
use std::collections::HashMap;
use std::env;
use std::fs::File;
use std::io;
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use std::process;
use target_lexicon::{Architecture, Environment, OperatingSystem, Triple, X86_32Architecture};
use crate::BuildRequest;
const DEFAULT_ICON: &[u8] = include_bytes!("../../assets/icon.ico");
impl BuildRequest {
pub(crate) fn winres_dir(&self) -> PathBuf {
self.platform_dir().join(".winres")
}
pub(crate) fn winres_linker_args(&self) -> Option<(String, String)> {
if !matches!(self.triple.operating_system, OperatingSystem::Windows) {
return None;
}
let winres_dir = self.winres_dir();
let (link_spec, lib_filename) = if matches!(self.triple.environment, Environment::Msvc) {
("dylib=resource", "resource.lib")
} else {
("static=resource", "libresource.a")
};
if !winres_dir.join(lib_filename).is_file() {
return None;
}
Some((
winres_dir.to_string_lossy().to_string(),
link_spec.to_string(),
))
}
pub(crate) fn write_winres(&self) -> Result<()> {
let bundle = &self.config.bundle;
let package = self.package();
let (version_str, version) = match bundle.version.as_ref() {
Some(v) => (v, VersionInfo::version_from_str(v)),
None => (
&format!(
"{}.{}.{}",
package.version.major, package.version.minor, package.version.patch
),
VersionInfo::version_from_krate(&package.version),
),
};
let (file_version_str, file_version) = match bundle.file_version.as_ref() {
Some(v) => (v, VersionInfo::version_from_str(v)),
None => (version_str, version),
};
let productname = match self.config.application.name.as_ref() {
Some(n) => n,
None => &self.bundled_app_name(),
};
let binding = package.description.clone().unwrap_or_default();
let description = match bundle.short_description.as_ref() {
Some(val) => val,
None => bundle.long_description.as_ref().unwrap_or(&binding),
};
let winres_dir = self.winres_dir();
_ = std::fs::create_dir_all(&winres_dir);
let mut winres = WindowsResource::new(version, file_version);
winres
.set(Properties::ProductVersion, version_str)
.set(Properties::FileVersion, file_version_str)
.set(Properties::ProductName, productname)
.set(Properties::FileDescription, description);
if let Some(value) = &bundle.original_file_name {
winres.set(Properties::OriginalFilename, value);
}
if let Some(value) = &bundle.copyright {
winres.set(Properties::Copyright, value);
}
if let Some(value) = &bundle.trademark {
winres.set(Properties::Trademark, value);
}
if let Some(value) = &bundle.publisher {
winres.set(Properties::CompanyName, value);
}
if let Some(value) = &bundle.category {
winres.set(Properties::Other("Category"), value);
}
let windows = bundle.windows.as_ref();
if let Some(value) = windows.and_then(|w| w.comments.as_deref()) {
winres.set(Properties::Comments, value);
}
if let Some(value) = windows.and_then(|w| w.internal_name.as_deref()) {
winres.set(Properties::InternalName, value);
}
if let Some(language) = windows.and_then(|w| w.language) {
winres.set_language(language);
}
if windows.and_then(|w| w.add_toolkit_include).unwrap_or(false) {
winres.add_toolkit_include(true);
}
if let Some(value) = windows.and_then(|w| w.extra_rc.as_deref()) {
winres.append_rc_content(value);
}
let mut has_default = false;
if let Some(path) = windows.and_then(|w| w.icon_path.as_ref()) {
winres.set_icon(path.clone());
has_default = true;
}
if let Some(icons) = bundle.icon.as_ref() {
for (id, icon) in icons.iter().enumerate() {
if icon.ends_with(".ico") {
let icon_path = self.canonicalize_icon_path(&PathBuf::from(icon))?;
if !has_default {
winres.set_icon(icon_path);
has_default = true;
} else {
winres.set_icon_with_id(icon_path, id.to_string());
};
}
}
}
if !has_default {
let default_icon = winres_dir.join("icon.ico");
let mut file = File::create(&default_icon)?;
file.write_all(DEFAULT_ICON)?;
let icon = self.canonicalize_icon_path(&default_icon)?;
winres.set_icon(icon);
}
winres.compile(&self.triple, &winres_dir)?;
Ok(())
}
fn canonicalize_icon_path(&self, icon_path: &PathBuf) -> Result<PathBuf> {
if icon_path.is_absolute() && icon_path.is_file() {
return Ok(dunce::canonicalize(icon_path)?);
}
let crate_icon = self.crate_dir().join(icon_path);
if crate_icon.is_file() {
return Ok(dunce::canonicalize(crate_icon)?);
}
let workspace_icon = self.workspace_dir().join(icon_path);
if workspace_icon.is_file() {
return Ok(dunce::canonicalize(workspace_icon)?);
}
Err(anyhow::anyhow!(
"Could not find icon from path {:?}",
icon_path
))
}
}
#[allow(clippy::upper_case_acronyms)]
#[derive(PartialEq, Eq, Hash, Debug, Clone, Copy)]
pub enum IDI {
APPLICATION = 32512,
ERROR = 32513,
QUESTION = 32514,
WARNING = 32515,
INFORMATION = 32516,
WINLOGO = 32517,
SHIELD = 32518,
}
impl std::fmt::Display for IDI {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", *self as u32)
}
}
impl From<IDI> for String {
fn from(e: IDI) -> Self {
e.to_string()
}
}
#[allow(clippy::upper_case_acronyms)]
#[derive(PartialEq, Eq, Hash, Debug)]
pub enum VersionInfo {
FILEVERSION,
PRODUCTVERSION,
FILEOS,
FILETYPE,
FILESUBTYPE,
FILEFLAGSMASK,
FILEFLAGS,
}
#[derive(PartialEq, Eq, Hash, Debug)]
pub enum Properties {
FileVersion,
FileDescription,
ProductVersion,
ProductName,
OriginalFilename,
Copyright,
Trademark,
CompanyName,
Comments,
InternalName,
Other(&'static str),
}
impl std::fmt::Display for Properties {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Properties::FileVersion => write!(f, "FileVersion"),
Properties::FileDescription => write!(f, "FileDescription"),
Properties::ProductVersion => write!(f, "ProductVersion"),
Properties::ProductName => write!(f, "ProductName"),
Properties::OriginalFilename => write!(f, "OriginalFilename"),
Properties::Copyright => write!(f, "LegalCopyright"),
Properties::Trademark => write!(f, "LegalTrademark"),
Properties::CompanyName => write!(f, "CompanyName"),
Properties::Comments => write!(f, "Comments"),
Properties::InternalName => write!(f, "InternalName"),
Properties::Other(other) => write!(f, "{}", other),
}
}
}
impl VersionInfo {
fn version_from_str(version: &str) -> u64 {
let parts: Vec<&str> = version.split('.').collect();
if parts.len() > 4 {
tracing::warn!("Version number had more than 4 parts. Ignoring the rest.");
}
let mut segments = [0u16; 4];
for (i, part) in parts.iter().take(4).enumerate() {
match part.parse::<u16>() {
Ok(value) => segments[i] = value,
Err(e) => {
tracing::warn!(
"Could not parse segment {} '{}' as u16: {}. Defaulting to 0.",
i,
part,
e
);
}
}
}
((segments[0] as u64) << 48)
| ((segments[1] as u64) << 32)
| ((segments[2] as u64) << 16)
| (segments[3] as u64)
}
fn version_from_krate(v: &Version) -> u64 {
(v.major << 48) | (v.minor << 32) | (v.patch << 16)
}
}
#[derive(Debug)]
struct Icon {
path: String,
name_id: String,
}
#[derive(Debug)]
struct WindowsResource {
properties: HashMap<String, String>,
version_info: HashMap<VersionInfo, u64>,
icons: Vec<Icon>,
language: u16,
add_toolkit_include: bool,
append_rc_content: String,
}
impl WindowsResource {
fn new(version: u64, file_version: u64) -> Self {
let props: HashMap<String, String> = HashMap::new();
let mut ver: HashMap<VersionInfo, u64> = HashMap::new();
ver.insert(VersionInfo::FILEVERSION, file_version);
ver.insert(VersionInfo::PRODUCTVERSION, version);
ver.insert(VersionInfo::FILEOS, 0x00040004); ver.insert(VersionInfo::FILETYPE, 1); ver.insert(VersionInfo::FILESUBTYPE, 0); ver.insert(VersionInfo::FILEFLAGSMASK, 0x3F); ver.insert(VersionInfo::FILEFLAGS, 0);
WindowsResource {
properties: props,
version_info: ver,
icons: Vec::new(),
language: 0,
add_toolkit_include: false,
append_rc_content: String::new(),
}
}
fn set(&mut self, name: Properties, value: &str) -> &mut Self {
self.properties.insert(name.to_string(), value.to_string());
self
}
fn set_language(&mut self, language: u16) -> &mut Self {
self.language = language;
self
}
fn set_icon(&mut self, path: PathBuf) -> &mut Self {
self.set_icon_with_id(path, IDI::APPLICATION)
}
fn set_icon_with_id(&mut self, path: PathBuf, name_id: impl Into<String>) -> &mut Self {
self.icons.push(Icon {
path: path.to_string_lossy().to_string(),
name_id: name_id.into(),
});
self
}
fn add_toolkit_include(&mut self, add: bool) -> &mut Self {
self.add_toolkit_include = add;
self
}
fn write_resource_file(&self, path: &Path) -> Result<PathBuf> {
let path = path.join("resource.rc");
let mut f = File::create(&path)?;
writeln!(f, "#pragma code_page(65001)")?;
writeln!(f, "1 VERSIONINFO")?;
for (k, v) in self.version_info.iter() {
match *k {
VersionInfo::FILEVERSION | VersionInfo::PRODUCTVERSION => writeln!(
f,
"{:?} {}, {}, {}, {}",
k,
(*v >> 48) as u16,
(*v >> 32) as u16,
(*v >> 16) as u16,
*v as u16
)?,
_ => writeln!(f, "{:?} {:#x}", k, v)?,
};
}
writeln!(f, "{{\nBLOCK \"StringFileInfo\"")?;
writeln!(f, "{{\nBLOCK \"{:04x}04b0\"\n{{", self.language)?;
for (k, v) in self.properties.iter() {
if !v.is_empty() {
writeln!(
f,
"VALUE \"{}\", \"{}\"",
escape_string_windows(k),
escape_string_windows(v)
)?;
}
}
writeln!(f, "}}\n}}")?;
writeln!(f, "BLOCK \"VarFileInfo\" {{")?;
writeln!(f, "VALUE \"Translation\", {:#x}, 0x04b0", self.language)?;
writeln!(f, "}}\n}}")?;
for icon in &self.icons {
writeln!(
f,
"{} ICON \"{}\"",
escape_string_windows(&icon.name_id),
escape_string_windows(&icon.path)
)?;
}
writeln!(f, "{}", self.append_rc_content)?;
Ok(path)
}
fn append_rc_content(&mut self, content: &str) -> &mut Self {
if !(self.append_rc_content.ends_with('\n') || self.append_rc_content.is_empty()) {
self.append_rc_content.push('\n');
}
self.append_rc_content.push_str(content);
self
}
fn compile(&mut self, target: &Triple, output_dir: &Path) -> Result<()> {
if matches!(target.environment, Environment::Msvc) {
tracing::debug!("Compiling Windows resource file with msvc toolkit");
self.compile_with_toolkit_msvc(target, output_dir)
} else if target.environment.to_string().contains("gnu") {
tracing::debug!("Compiling Windows resource file with gnu toolkit");
self.compile_with_toolkit_gnu(target, output_dir)
} else {
Err(anyhow!(
"Can only compile resource file when target_env is 'gnu' or 'msvc'",
))
}
}
fn compile_with_toolkit_gnu(&mut self, target: &Triple, output_dir: &Path) -> Result<()> {
let toolkit_path =
if env::var_os("HOST").is_some_and(|v| v.to_string_lossy().contains("windows")) {
PathBuf::from("\\")
} else {
PathBuf::from("/")
};
let prefix = if env::var_os("HOST")
.zip(env::var_os("TARGET"))
.is_some_and(|(h, t)| h != t)
{
match (target.architecture, target.environment) {
(Architecture::X86_64, Environment::Gnu) => "x86_64-w64-mingw32-",
(Architecture::X86_32(X86_32Architecture::I686), Environment::Gnu) => {
"i686-w64-mingw32-"
}
(Architecture::X86_32(X86_32Architecture::I586), Environment::Gnu) => {
"i586-w64-mingw32-"
}
(Architecture::Aarch64(_), Environment::Gnu) => "llvm-",
(_, Environment::GnuLlvm) => "llvm-",
_ => {
tracing::warn!("Unknown Windows target used for cross-compilation - invoking unprefixed windres");
""
}
}
} else {
""
};
let windres_path = if let Ok(windres) = env::var("WINDRES") {
windres
} else {
format!("{}windres", prefix)
};
let ar_path = if let Ok(ar) = env::var("AR") {
ar
} else {
format!("{}ar", prefix)
};
let rc_file = self.write_resource_file(output_dir)?;
tracing::debug!("Input file: '{}'", rc_file.display());
let output = output_dir.join("resource.o");
tracing::debug!("Output object file: '{}'", output.display());
tracing::debug!("Selected toolkit path: '{}'", &toolkit_path.display());
tracing::debug!("Selected windres path: '{:?}'", &windres_path);
tracing::debug!("Selected ar path: '{:?}'", &ar_path);
let status = process::Command::new(&windres_path)
.current_dir(&toolkit_path)
.arg(format!("{}", rc_file.display()))
.arg(format!("{}", output.display()))
.output()?;
if !status.status.success() {
return Err(anyhow!("Compiling resource file {:?}", &status.stderr));
}
let libname = output_dir.join("libresource.a");
tracing::debug!("Output lib file: '{}'", output.display());
let status = process::Command::new(&ar_path)
.current_dir(&toolkit_path)
.arg("rsc")
.arg(format!("{}", libname.display()))
.arg(format!("{}", output.display()))
.output()?;
if !status.status.success() {
return Err(anyhow!(
"Creating static library for resource file {:?}",
&status.stderr
));
}
Ok(())
}
fn compile_with_toolkit_msvc(&mut self, target: &Triple, output_dir: &Path) -> Result<()> {
let toolkit = get_sdk(matches!(target.architecture, Architecture::X86_64))?;
let rc_file = self.write_resource_file(output_dir)?;
tracing::debug!("Selected toolkit path: '{}'", toolkit.display());
tracing::debug!("Input file: '{}'", rc_file.display());
let output = output_dir.join("resource.lib");
tracing::debug!("Output file: '{}'", output.display());
let mut command = process::Command::new(&toolkit);
if self.add_toolkit_include {
let root = win_sdk_include_root(&toolkit);
tracing::debug!("Adding toolkit include: {}", root.display());
command.arg(format!("/I{}", root.join("um").display()));
command.arg(format!("/I{}", root.join("shared").display()));
}
let status = command
.arg(format!("/fo{}", output.display()))
.arg(format!("{}", rc_file.display()))
.output()?;
if !status.status.success() {
return Err(anyhow!("Compiling resource file {:?}", &status.stderr));
}
Ok(())
}
}
fn get_sdk(is_x64: bool) -> io::Result<PathBuf> {
let output = process::Command::new("reg")
.arg("query")
.arg(r"HKLM\SOFTWARE\Microsoft\Windows Kits\Installed Roots")
.arg("/reg:32")
.output()?;
if !output.status.success() {
return Err(io::Error::other(format!(
"Querying the registry failed with error message:\n{}",
String::from_utf8(output.stderr).map_err(|e| io::Error::other(e.to_string()))?
)));
}
let lines = String::from_utf8(output.stdout).map_err(|e| io::Error::other(e.to_string()))?;
let mut lines: Vec<&str> = lines.lines().collect();
lines.reverse();
for line in lines {
if line.trim().starts_with("KitsRoot") {
let kit: String = line
.chars()
.skip(line.find("REG_SZ").unwrap() + 6)
.skip_while(|c| c.is_whitespace())
.collect();
let p = PathBuf::from(&kit);
let rc = if is_x64 {
p.join(r"bin\x64\rc.exe")
} else {
p.join(r"bin\x86\rc.exe")
};
if rc.exists() {
return Ok(rc);
}
if let Ok(bin) = p.join("bin").read_dir() {
for e in bin.filter_map(|e| e.ok()) {
let p = if is_x64 {
e.path().join(r"x64\rc.exe")
} else {
e.path().join(r"x86\rc.exe")
};
if p.exists() {
return Ok(p);
}
}
}
}
}
Err(io::Error::other("Can not find Windows SDK"))
}
fn escape_string_windows(string: &str) -> String {
let mut escaped = String::new();
for chr in string.chars() {
match chr {
'"' => escaped.push_str("\"\""),
'\'' => escaped.push_str("\\'"),
'\\' => escaped.push_str("\\\\"),
'\n' => escaped.push_str("\\n"),
'\t' => escaped.push_str("\\t"),
'\r' => escaped.push_str("\\r"),
_ => escaped.push(chr),
};
}
escaped
}
fn win_sdk_include_root(path: &Path) -> PathBuf {
let mut tools_path = PathBuf::new();
let mut iter = path.iter();
while let Some(p) = iter.next() {
if p == "bin" {
let version = iter.next().unwrap();
tools_path.push("Include");
if version.to_string_lossy().starts_with("10.") {
tools_path.push(version);
}
break;
} else {
tools_path.push(p);
}
}
tools_path
}