//! 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(); let install_dir = Path::new(&out_dir).join("install"); 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??"); } // 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 shell dlls 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??"); } } else if target_os == "windows-gnu" { todo!("need to run configure in bash but how to find bash??") } else { let unix_dir = tcl_build_path.join("unix"); let configure = Command::new("./configure") .args(&[ &format!("--prefix={}", install_dir.to_str().unwrap()), "--enable-shared=no", ]) .current_dir(&unix_dir) .status() .unwrap(); if !configure.success() { panic!("configure failed??"); } let make = Command::new("make") .args(&["binaries", "install"]) .current_dir(&unix_dir) .status() .unwrap(); if !make.success() { panic!("make failed??"); } } install_dir } fn link_tcl(target_os: &str) { if target_os == "windows-msvc" { println!("cargo:rustc-link-lib=static=tcl86tsx"); println!("cargo:rustc-link-lib=static=tclstub86"); } else if target_os == "windows-gnu" { todo!("which things get built under msys") } else { println!("cargo:rustc-link-lib=static=tcl8.6"); println!("cargo:rustc-link-lib=static=tclstub8.6"); } // 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 { println!("cargo:rustc-link-lib=z"); // TODO does this always get used or only sometimes } } 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); compress_stdlib(tcl_lib_path.join("tcl8.6")); 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 compress_stdlib(stdlib_root: PathBuf) { let out_dir = env::var("OUT_DIR").unwrap(); let tcl_stdlib_archive_path = Path::new(&out_dir).join("stdlib.tar.gz"); let writer = flate2::write::GzEncoder::new( fs::File::create(tcl_stdlib_archive_path).unwrap(), flate2::Compression::best(), ); let mut ar = tar::Builder::new(writer); for child in stdlib_root.read_dir().unwrap() { let child = child.unwrap(); if child.path().is_dir() { ar.append_dir_all(child.file_name(), child.path()).unwrap(); } else { ar.append_path_with_name(child.path(), child.file_name()) .unwrap(); } } } fn get_os_from_triple(triple: &str) -> Option<&str> { triple.splitn(3, "-").nth(2) }