use std::{env, path::PathBuf, process};
use error_stack::{Result, ResultExt as _};
#[derive(thiserror::Error, Debug)]
enum Error {
#[error("failed to vendor {name} into {}", .out_dir.display())]
VendorDep {
name: &'static str,
out_dir: PathBuf,
},
#[cfg(unix)]
#[error("failed to fix file permissions for Ninja")]
FixNinjaPermissions,
#[error("failed to build libui-ng")]
BuildLibui,
#[error("failed to link required system libraries")]
LinkSystemLibs,
#[error("failed to generate bindings")]
GenBindings,
}
fn main() -> process::ExitCode {
match main_impl() {
Ok(_) => process::ExitCode::SUCCESS,
Err(e) => {
eprintln!("{e:?}");
process::ExitCode::FAILURE
}
}
}
fn main_impl() -> Result<(), Error> {
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let libui_dir = out_dir.join("libui-ng");
let meson_dir = out_dir.join("meson");
let ninja_dir = out_dir.join("ninja");
let vendor_dep = |name, dir| {
dep::sync(name, dir)
.change_context_lazy(|| Error::VendorDep { name, out_dir: out_dir.clone() })
};
vendor_dep("libui-ng", libui_dir.as_path())?;
let strategy = if env::var("DOCS_RS").is_ok() {
Strategy::GenBindingsOnly
} else if cfg!(feature = "probe-system-libui") {
let maybe_lib = pkg_config::Config::new()
.cargo_metadata(false)
.env_metadata(true)
.statik(false)
.probe("libui-ng");
match maybe_lib {
Ok(lib) => Strategy::UseSystem { link_paths: lib.link_paths },
Err(_) => Strategy::BuildFromSource,
}
} else {
Strategy::BuildFromSource
};
match strategy {
Strategy::UseSystem { ref link_paths } => {
for path in link_paths {
println!("cargo:rustc-link-search=native={}", path.display());
}
println!("cargo:rustc-link-lib=dylib=ui");
}
Strategy::BuildFromSource => {
let backend = build::Backend::from_cfg();
vendor_dep("meson", meson_dir.as_path())?;
if matches!(backend, build::Backend::Ninja) {
vendor_dep("ninja", ninja_dir.as_path())?;
#[cfg(unix)]
unix::mark_executable(ninja_dir.join("src/inline.sh"))
.change_context(Error::FixNinjaPermissions)?;
}
let libui_build_dir = backend
.build_libui(&libui_dir, &meson_dir, &ninja_dir)
.change_context(Error::BuildLibui)
.map_err(|report| {
#[cfg(target_os = "windows")]
{
report.attach_printable(
"Have you tried building libui-ng-sys from the Microsoft Developer \
Command Prompt or Developer Powershell?"
)
}
#[cfg(not(target_os = "windows"))]
report
})?;
println!("cargo:rustc-link-search=native={}", libui_build_dir.display());
println!("cargo:rustc-link-lib=static=ui");
link_system_libs().change_context(Error::LinkSystemLibs)?;
}
Strategy::GenBindingsOnly => {
}
}
bindings::generate(&libui_dir, &out_dir).change_context(Error::GenBindings)?;
println!("cargo:rerun-if-changed=build.rs");
Ok(())
}
#[derive(Clone)]
enum Strategy {
UseSystem {
link_paths: Vec<PathBuf>,
},
BuildFromSource,
GenBindingsOnly,
}
#[derive(thiserror::Error, Debug)]
enum LinkSystemLibsError {
#[cfg(target_os = "macos")]
#[error("failed to link clang_rt")]
ClangRt,
#[cfg(target_os = "linux")]
#[error("failed to link GTK")]
Gtk,
}
fn link_system_libs() -> Result<(), LinkSystemLibsError> {
#[allow(unused)]
macro_rules! link {
($kind:literal : $($name:literal)*) => {
$(
println!(concat!("cargo:rustc-link-lib=", $kind, "=", $name));
)*
};
($($name:literal)*) => {
$(
println!(concat!("cargo:rustc-link-lib=", $name));
)*
};
}
#[cfg(target_os = "macos")]
macro_rules! frameworks {
($($name:literal)*) => { link!("framework": $($name)*) };
}
#[allow(unused)]
macro_rules! dylibs {
($($name:literal)*) => { link!("dylib": $($name)*) };
}
#[cfg(target_os = "macos")]
{
use error_stack::IntoReport as _;
frameworks! {
"AppKit"
"Foundation"
}
#[derive(thiserror::Error, Debug)]
enum LinkClangRtError {
#[error("failed to spawn clang")]
SpawnClang,
}
let clang_rt_dir = process::Command::new("clang")
.arg("-print-runtime-dir")
.output()
.into_report()
.change_context(LinkClangRtError::SpawnClang)
.map(|out| unix::make_path_from_ascii(out.stdout))
.change_context(LinkSystemLibsError::ClangRt)?;
println!("cargo:rustc-link-search=native={}", clang_rt_dir.display());
link!("clang_rt.osx");
}
#[cfg(target_os = "linux")]
{
gtk::link().change_context(LinkSystemLibsError::Gtk)?;
}
#[cfg(target_os = "windows")]
{
dylibs! {
"comctl32"
"comdlg32"
"d2d1"
"dwrite"
"gdi32"
"kernel32"
"msimg32"
"ole32"
"oleacc"
"oleaut32"
"user32"
"uuid"
"uxtheme"
"windowscodecs"
};
}
Ok(())
}
#[cfg(unix)]
mod unix {
use std::{
ffi::OsString,
fmt,
fs,
os::unix::{ffi::OsStringExt as _, fs::PermissionsExt as _},
path::{Path, PathBuf},
};
use error_stack::{IntoReport as _, Result, ResultExt as _};
#[derive(Debug)]
pub struct MarkExecutableError {
path: PathBuf,
}
impl std::error::Error for MarkExecutableError {}
impl fmt::Display for MarkExecutableError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "failed to mark {} as executable", self.path.display())
}
}
pub fn mark_executable(path: impl AsRef<Path>) -> Result<(), MarkExecutableError> {
let path = path.as_ref();
fs::set_permissions(path, fs::Permissions::from_mode(0o755))
.into_report()
.change_context_lazy(|| MarkExecutableError { path: path.to_path_buf() })
}
#[allow(dead_code)]
pub fn make_path_from_ascii(mut path: Vec<u8>) -> PathBuf {
if let Some(newline_idx) = memchr::memchr(b'\n', path.as_slice()) {
path.truncate(newline_idx);
}
PathBuf::from(OsString::from_vec(path))
}
}
#[cfg(target_os = "linux")]
mod gtk {
use error_stack::{IntoReport as _, Result};
pub use pkg_config::Error;
static MIN_VERSION: &str = "3.10.0";
static PACKAGE: &str = "gtk+-3.0";
pub fn find() -> Result<pkg_config::Library, Error> {
probe(false)
}
pub fn link() -> Result<(), pkg_config::Error> {
probe(true).map(|_| ())
}
fn probe(should_link: bool) -> Result<pkg_config::Library, Error> {
pkg_config::Config::new()
.atleast_version(MIN_VERSION)
.print_system_cflags(should_link)
.print_system_libs(should_link)
.probe(PACKAGE)
.into_report()
}
}
mod dep {
use std::{fs, os, path::Path};
use error_stack::{IntoReport as _, Result, ResultExt as _};
#[derive(thiserror::Error, Debug)]
pub enum SyncError {
#[error("fs::create_dir() failed")]
CreateDir,
#[error("fs::read_dir() failed")]
ReadDir,
#[error("failed to read directory entry")]
DirEntry,
#[error("fs::DirEntry::file_type() failed")]
DirEntryFileType,
#[error("fs::copy() failed")]
Copy,
#[error("fs::read_link() failed")]
ReadLink,
#[error("failed to create symbolic link")]
Symlink,
}
pub fn sync(name: &str, to: &Path) -> Result<(), SyncError> {
copy_dir_contents(Path::new("dep").join(name), to)
}
fn copy_dir_contents(
from: impl AsRef<Path>,
to: impl AsRef<Path>,
) -> Result<(), SyncError> {
let to = to.as_ref();
if !to.exists() {
fs::create_dir(to).into_report().change_context(SyncError::CreateDir)?;
}
let maybe_entries = fs::read_dir(from.as_ref())
.into_report()
.change_context(SyncError::ReadDir)?;
for maybe_entry in maybe_entries {
let entry = maybe_entry.into_report().change_context(SyncError::DirEntry)?;
let kind = entry
.file_type()
.into_report()
.change_context(SyncError::DirEntryFileType)?;
let from = entry.path();
let to = to.join(entry.file_name());
if kind.is_dir() {
copy_dir_contents(from, to)?;
} else if kind.is_file() {
fs::copy(from, to).into_report().change_context(SyncError::Copy)?;
} else if kind.is_symlink() {
let original = fs::read_link(from)
.into_report()
.change_context(SyncError::ReadLink)?;
#[cfg(windows)]
{
use os::windows::fs;
if original.is_dir() {
fs::symlink_dir(original, to)
} else {
fs::symlink_file(original, to)
}
.into_report()
.change_context(SyncError::Symlink)?;
}
#[cfg(not(windows))]
{
#[cfg(unix)]
{
os::unix::fs::symlink(original, to)
.into_report()
.change_context(SyncError::Symlink)?;
}
#[cfg(not(unix))]
{
fs::copy(from, to).into_report().change_context(SyncError::Copy)?;
}
}
} else {
panic!("what is this thing?");
}
}
Ok(())
}
}
mod build {
use std::{env, path::{Path, PathBuf}, process};
use error_stack::{IntoReport as _, Result, ResultExt as _};
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("failed to setup libui-ng")]
SetupLibui,
#[error("failed to build Ninja")]
BuildNinja,
#[error("failed to compile libui-ng")]
CompileLibui,
#[cfg(target_os = "windows")]
#[error("failed to fix libui-ng archive filename")]
FixLibuiName,
}
#[derive(thiserror::Error, Debug)]
pub enum PythonError {
#[error("failed to run Python")]
RunPython,
#[error("{:?}", out)]
Python { out: process::Output },
}
impl Backend {
pub fn from_cfg() -> Self {
if cfg!(feature = "build-with-msvc") {
Self::Msvc
} else if cfg!(feature = "build-with-xcode") {
Self::Xcode
} else {
Self::Ninja
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Backend {
Msvc,
Ninja,
Xcode,
}
impl Backend {
pub fn build_libui(
self,
libui_dir: &Path,
meson_dir: &Path,
ninja_dir: &Path,
) -> Result<PathBuf, Error> {
if let Self::Ninja = self {
Self::build_ninja(ninja_dir).change_context(Error::BuildNinja)?;
}
self.setup_libui(libui_dir, meson_dir, ninja_dir).change_context(Error::SetupLibui)?;
self.compile_libui(libui_dir, meson_dir, ninja_dir)
.change_context(Error::CompileLibui)?;
let build_dir = libui_dir.join("build/meson-out");
#[cfg(target_os = "windows")]
self.fix_libui_name(build_dir.as_path())
.change_context(Error::FixLibuiName)?;
Ok(build_dir)
}
fn ninja_path(ninja_dir: &Path) -> PathBuf {
let ext = env::consts::EXE_EXTENSION;
ninja_dir.join("ninja").with_extension(ext)
}
fn run_python(
f: impl Fn(&mut process::Command),
ninja_dir: Option<&Path>,
) -> Result<(), PythonError> {
let mut cmd = process::Command::new("python3");
f(&mut cmd);
if let Some(dir) = ninja_dir {
cmd.env("NINJA", Self::ninja_path(dir));
}
let out = cmd.output().into_report().change_context(PythonError::RunPython)?;
if out.status.success() {
Ok(())
} else {
Err(error_stack::report!(PythonError::Python { out }))
}
}
fn build_ninja(ninja_dir: &Path) -> Result<(), PythonError> {
if Self::ninja_path(ninja_dir).exists() {
return Ok(());
}
Self::run_python(
|cmd| {
cmd
.arg("configure.py")
.arg("--bootstrap")
.current_dir(ninja_dir);
},
None,
)
}
fn setup_libui(
&self,
libui_dir: &Path,
meson_dir: &Path,
ninja_dir: &Path,
) -> Result<(), PythonError> {
Self::run_python(
|cmd| {
cmd
.arg(meson_dir.join("meson.py"))
.arg("setup")
.arg("--default-library=static")
.arg("--buildtype=release")
.arg(format!("--optimization={}", Self::optimization_level()))
.arg(format!("--backend={}", self.as_str()))
.arg(libui_dir.join("build"))
.arg(libui_dir);
},
Some(ninja_dir),
)
}
#[allow(dead_code)]
fn is_debug() -> bool {
!matches!(env::var("DEBUG").as_deref(), Ok("0" | "false"))
}
fn optimization_level() -> String {
let level = env::var("OPT_LEVEL").expect("$OPT_LEVEL is unset");
match level.as_str() {
"z" => String::from("s"),
_ => level,
}
}
fn as_str(&self) -> &'static str {
match self {
Self::Msvc => "vs",
Self::Ninja => "ninja",
Self::Xcode => "xcode",
}
}
fn compile_libui(
&self,
libui_dir: &Path,
meson_dir: &Path,
ninja_dir: &Path,
) -> Result<(), PythonError> {
Self::run_python(
|cmd| {
cmd
.arg(meson_dir.join("meson.py"))
.arg("compile")
.arg(format!("-C={}", libui_dir.join("build").display()));
},
Some(ninja_dir),
)
}
#[cfg(target_os = "windows")]
fn fix_libui_name(&self, build_dir: &Path) -> Result<(), std::io::Error> {
use std::fs;
let old_path = build_dir.join("libui.a");
let new_path = build_dir.join("ui.lib");
if old_path.exists() {
if new_path.exists() {
let _ = fs::remove_file(new_path.as_path());
}
fs::hard_link(old_path, new_path)?;
} else {
assert_eq!(*self, Self::Msvc);
}
Ok(())
}
}
}
mod bindings {
use std::{borrow::Cow, fmt, iter, path::{Path, PathBuf}};
use error_stack::{IntoReport as _, Result, ResultExt as _};
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("failed to generate bindings")]
Generate,
#[error("failed to write bindings to {}", .to.display())]
WriteToFile {
to: PathBuf,
},
}
pub fn generate(libui_dir: &Path, out_dir: &Path) -> Result<(), Error> {
Header::main().generate(libui_dir, out_dir)?;
Header::control_sigs().generate(libui_dir, out_dir)?;
#[cfg(target_os = "macos")]
Header::darwin().generate(libui_dir, out_dir)?;
#[cfg(target_os = "linux")]
Header::unix().generate(libui_dir, out_dir)?;
#[cfg(target_os = "windows")]
Header::windows().generate(libui_dir, out_dir)?;
Ok(())
}
struct Header {
lang: Language,
include_stmts: Vec<IncludeStmt>,
filename: String,
blocklists_main: bool,
}
impl Header {
fn main() -> Self {
Self {
lang: Language::C,
include_stmts: vec![
IncludeStmt {
flavor: IncludeStmtFlavor::Include,
scope: IncludeStmtScope::Local,
arg: "ui.h".to_string(),
},
],
filename: "bindings".to_string(),
blocklists_main: false,
}
}
fn control_sigs() -> Self {
Self {
lang: Language::C,
include_stmts: vec![
IncludeStmt {
flavor: IncludeStmtFlavor::Include,
scope: IncludeStmtScope::Local,
arg: "common/controlsigs.h".to_string(),
},
],
filename: "bindings-control-sigs".to_string(),
blocklists_main: true,
}
}
#[cfg(target_os = "macos")]
fn darwin() -> Self {
Self::ext(
"darwin",
Language::ObjC,
iter::once({
IncludeStmt {
flavor: IncludeStmtFlavor::Import,
scope: IncludeStmtScope::System,
arg: "Cocoa/Cocoa.h".into(),
}
})
)
}
#[cfg(target_os = "linux")]
fn unix() -> Self {
Self::ext(
"unix",
Language::C,
iter::once({
IncludeStmt {
flavor: IncludeStmtFlavor::Include,
scope: IncludeStmtScope::System,
arg: "gtk/gtk.h".into(),
}
})
)
}
#[cfg(target_os = "windows")]
fn windows() -> Self {
Self::ext(
"windows",
Language::C,
iter::once({
IncludeStmt {
flavor: IncludeStmtFlavor::Include,
scope: IncludeStmtScope::System,
arg: "windows.h".into(),
}
})
)
}
fn ext(
name: impl fmt::Display,
lang: Language,
deps: impl IntoIterator<Item = IncludeStmt>,
) -> Self {
Self {
lang,
include_stmts: iter::once({
IncludeStmt {
flavor: IncludeStmtFlavor::Include,
scope: IncludeStmtScope::Local,
arg: "ui.h".to_string(),
}
})
.chain(deps)
.chain(iter::once({
IncludeStmt {
flavor: IncludeStmtFlavor::Include,
scope: IncludeStmtScope::Local,
arg: format!("ui_{}.h", name),
}
}))
.collect(),
filename: format!("bindings-{}", name),
blocklists_main: true,
}
}
fn generate(self, libui_dir: &Path, out_dir: &Path) -> Result<(), Error> {
static LIBUI_REGEX: &str = "ui(?:[A-Z][a-z0-9]*)*";
let mut builder = bindgen::builder()
.header_contents("wrapper.h", &self.contents(libui_dir))
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
.allowlist_recursively(false)
.allowlist_function(LIBUI_REGEX)
.allowlist_type(LIBUI_REGEX)
.allowlist_var(LIBUI_REGEX);
if self.blocklists_main {
builder = builder.blocklist_file(".*ui\\.h");
}
let bindings = builder
.clang_args(ClangArgs::new().as_args())
.clang_arg("-x")
.clang_arg(match self.lang {
Language::C => "c",
#[cfg(target_os = "macos")]
Language::ObjC => "objective-c",
})
.layout_tests(false)
.generate()
.map_err(|_| Error::Generate)?;
bindings.emit_warnings();
let to = PathBuf::from(format!("{}.rs", self.filename));
bindings
.write_to_file(out_dir.join(to.as_path()))
.into_report()
.change_context_lazy(|| Error::WriteToFile { to })
}
fn contents(&self, libui_dir: &Path) -> String {
self
.include_stmts
.iter()
.map(|stmt| stmt.to_string(libui_dir))
.collect::<Vec<String>>()
.join("\n")
}
}
enum Language {
C,
#[cfg(target_os = "macos")]
ObjC,
}
struct IncludeStmt {
flavor: IncludeStmtFlavor,
scope: IncludeStmtScope,
arg: String,
}
enum IncludeStmtFlavor {
Include,
#[cfg(target_os = "macos")]
Import,
}
enum IncludeStmtScope {
System,
Local,
}
impl IncludeStmt {
fn to_string(&self, libui_dir: &Path) -> String {
format!(
"#{} {}",
match self.flavor {
IncludeStmtFlavor::Include => "include",
#[cfg(target_os = "macos")]
IncludeStmtFlavor::Import => "import",
},
match self.scope {
IncludeStmtScope::System => format!("<{}>", self.arg),
IncludeStmtScope::Local => format!(
"\"{}\"",
libui_dir.join(&self.arg).display(),
),
},
)
}
}
struct ClangArgs {
defines: Vec<ClangDefine>,
include_paths: Vec<PathBuf>,
isysroot: Option<Cow<'static, Path>>,
}
struct ClangDefine {
key: String,
value: Option<String>,
}
impl ClangArgs {
fn new() -> Self {
#[cfg(target_os = "macos")]
return Self::macos();
#[cfg(target_os = "linux")]
return Self::linux();
#[cfg(target_os = "windows")]
return Self::windows();
#[allow(unreachable_code)]
{
unimplemented!("unsupported target OS");
}
}
#[cfg(target_os = "macos")]
fn macos() -> Self {
use std::process;
static DEFAULT_SDK_PATH: &str = concat!(
"/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer",
"/SDKs/MacOSX.sdk",
);
let sdk_path = process::Command::new("xcrun")
.args(["--sdk", "macosx", "--show-sdk-path"])
.output()
.ok()
.map(|out| crate::unix::make_path_from_ascii(out.stdout))
.map(Cow::Owned)
.unwrap_or_else(|| Cow::Borrowed(Path::new(DEFAULT_SDK_PATH)));
Self {
defines: Vec::new(),
include_paths: Vec::new(),
isysroot: Some(sdk_path),
}
}
#[cfg(target_os = "linux")]
fn linux() -> Self {
let gtk = crate::gtk::find().unwrap();
let defines = gtk
.defines
.into_iter()
.map(|(key, value)| {
ClangDefine { key, value }
})
.collect();
Self {
defines,
include_paths: gtk.include_paths,
isysroot: None,
}
}
#[cfg(target_os = "windows")]
fn windows() -> Self {
Self {
defines: Vec::new(),
include_paths: Vec::new(),
isysroot: None,
}
}
fn as_args(self) -> Vec<String> {
let defines = self
.defines
.into_iter()
.flat_map(|define| {
vec![
"-D".to_string(),
format!(
"{}{}",
define.key,
define.value.map(|it| format!("={}", it)).unwrap_or_default(),
),
]
});
let includes = self
.include_paths
.into_iter()
.flat_map(|path| {
vec![
"-I".to_string(),
path.display().to_string(),
]
});
let isysroot = self
.isysroot
.into_iter()
.flat_map(|path| {
vec![
"-isysroot".to_string(),
path.display().to_string(),
]
});
defines.chain(includes).chain(isysroot).collect()
}
}
}