//! derived from https://github.com/Rust-SDL2/rust-sdl2/blob/master/sdl2-sys/build.rs use std::path::{Path, PathBuf}; use std::process::Command; use std::{env, fs}; // means the latest stable version that can be downloaded from Tcl's source const LATEST_TCL_VERSION: &str = "8.6.11"; macro_rules! add_msvc_includes_to_bindings { ($bindings:expr) => { $bindings = $bindings.clang_arg(format!( "-IC:/Program Files (x86)/Windows Kits/8.1/Include/shared" )); $bindings = $bindings.clang_arg(format!("-IC:/Program Files/LLVM/lib/clang/5.0.0/include")); $bindings = $bindings.clang_arg(format!( "-IC:/Program Files (x86)/Windows Kits/10/Include/10.0.10240.0/ucrt" )); $bindings = $bindings.clang_arg(format!( "-IC:/Program Files (x86)/Microsoft Visual Studio 14.0/VC/include" )); $bindings = $bindings.clang_arg(format!( "-IC:/Program Files (x86)/Windows Kits/8.1/Include/um" )); }; } fn run_command(cmd: &str, args: &[&str]) { match Command::new(cmd).args(args).output() { Ok(output) => { if !output.status.success() { let error = std::str::from_utf8(&output.stderr).unwrap(); panic!("Command '{}' failed: {}", cmd, error); } } Err(error) => { panic!("Error running command '{}': {:#}", cmd, error); } } } fn download_to(url: &str, dest: &str) { if cfg!(windows) { run_command( "powershell", &[ "-NoProfile", "-NonInteractive", "-Command", &format!( "& {{ $client = New-Object System.Net.WebClient $client.DownloadFile(\"{0}\", \"{1}\") if (!$?) {{ Exit 1 }} }}", url, dest ) .as_str(), ], ); } else { run_command("curl", &[url, "-o", dest]); } } // returns the location of the downloaded source fn download_tcl() -> PathBuf { let out_dir = env::var("OUT_DIR").unwrap(); let tcl_archive_name = format!("tcl{}-src.tar.gz", LATEST_TCL_VERSION); let tcl_archive_url = format!( "https://iweb.dl.sourceforge.net/project/tcl/Tcl/{}/{}", LATEST_TCL_VERSION, tcl_archive_name ); let tcl_archive_path = Path::new(&out_dir).join(tcl_archive_name); let tcl_build_path = Path::new(&out_dir).join(format!("tcl{}", LATEST_TCL_VERSION)); // avoid re-downloading the archive if it already exists if !tcl_archive_path.exists() { download_to(&tcl_archive_url, tcl_archive_path.to_str().unwrap()); } let reader = flate2::read::GzDecoder::new(fs::File::open(&tcl_archive_path).unwrap()); let mut ar = tar::Archive::new(reader); ar.unpack(&out_dir).unwrap(); tcl_build_path } // compile a static lib fn compile_tcl(tcl_build_path: &Path, target_os: &str) -> PathBuf { let out_dir = env::var("OUT_DIR").unwrap(); if target_os == "windows-msvc" { let vswhere = Command::new(r"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe") .args(&[ "-products", "*", "-prerelease", "-latest", "-property", "installationPath", ]) .output() .unwrap(); if !vswhere.status.success() { panic!("vswhere failed??"); } let output = String::from_utf8(vswhere.stdout).unwrap(); let vs_root = output.trim(); if vs_root.lines().count() != 1 { panic!("vswhere found multiple roots??"); } let install_dir = Path::new(&out_dir).join("install"); // TODO i am going to stab a bitch why is this the easiest way to do this let bat = format!( r#" call "{}\Common7\Tools\VSDevCmd.bat" -arch=amd64 -host_arch=amd64 nmake -nologo -f makefile.vc release install OPTS=msvcrt,static,staticpkg "INSTALLDIR={}" "#, vs_root, install_dir.to_str().unwrap(), ); let bat_file = Path::new(&out_dir).join("build.bat"); fs::write(&bat_file, bat).unwrap(); let build = Command::new("cmd.exe") .arg("/c") .arg(bat_file.as_os_str()) .current_dir(tcl_build_path.join("win")) .env_remove("OUT_DIR") .status() .unwrap(); if !build.success() { panic!("nmake failed??") } install_dir } else { todo!() } } fn link_tcl(target_os: &str) { println!("cargo:rustc-link-lib=static=tcl86tsx"); println!("cargo:rustc-link-lib=static=tclstub86"); // Also linked to any required libraries for each supported platform if target_os.contains("windows") { println!("cargo:rustc-link-lib=user32"); println!("cargo:rustc-link-lib=netapi32"); } else { // TODO: Add other platform linker options here. } } fn main() { let target = env::var("TARGET").expect("Cargo build scripts always have TARGET"); let host = env::var("HOST").expect("Cargo build scripts always have HOST"); let target_os = get_os_from_triple(target.as_str()).unwrap(); let tcl_source_path = download_tcl(); println!("cargo:tcl-source={}", tcl_source_path.to_str().unwrap()); let tcl_compiled_path = compile_tcl(tcl_source_path.as_path(), target_os); let tcl_include_path = tcl_compiled_path.join("include"); let tcl_lib_path = tcl_compiled_path.join("lib"); println!("cargo:rustc-link-search={}", tcl_lib_path.to_str().unwrap()); let include_paths = vec![String::from(tcl_include_path.to_str().unwrap())]; println!("cargo:include={}", include_paths.join(":")); generate_bindings(target.as_str(), host.as_str(), include_paths.as_slice()); link_tcl(target_os); println!("cargo:rerun-if-changed=build.rs"); } // headers_path is a list of directories where the Tcl headers are expected // to be found by bindgen fn generate_bindings(target: &str, host: &str, headers_paths: &[String]) { let target_os = get_os_from_triple(target).unwrap(); let mut bindings = bindgen::Builder::default() // enable no_std-friendly output by only using core definitions .use_core() .default_enum_style(bindgen::EnumVariation::Rust { non_exhaustive: false, }) .ctypes_prefix("libc"); // Set correct target triple for bindgen when cross-compiling if target != host { bindings = bindings.clang_arg("-target"); bindings = bindings.clang_arg(target.clone()); } if headers_paths.len() == 0 { panic!("no headers"); } else { // if paths are included, use them for bindgen. Bindgen should use the first one. println!("cargo:include={}", headers_paths.join(":")); for headers_path in headers_paths { bindings = bindings.clang_arg(format!("-I{}", headers_path)); } } if target_os == "windows-msvc" { add_msvc_includes_to_bindings!(bindings); }; let bindings = bindings .header("wrapper.h") .allowlist_type("Tcl_.*") .allowlist_var("TCL_.*") .allowlist_function("Tcl_.*") .generate() .expect("Unable to generate bindings!"); let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); bindings .write_to_file(out_path.join("bindings.rs")) .expect("Couldn't write bindings!"); } fn get_os_from_triple(triple: &str) -> Option<&str> { triple.splitn(3, "-").nth(2) }