diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Cargo.toml | 15 | ||||
-rw-r--r-- | build.rs | 224 | ||||
-rw-r--r-- | src/lib.rs | 28 | ||||
-rw-r--r-- | wrapper.h | 1 |
5 files changed, 270 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6251462 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "tcl-sys" +version = "0.1.0" +authors = ["Melody Horn <melody@boringcactus.com>"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +libc = "0.2.94" + +[build-dependencies] +bindgen = "0.58.1" +flate2 = "1.0.20" +tar = "0.4.33" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..27736cd --- /dev/null +++ b/build.rs @@ -0,0 +1,224 @@ +//! 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(); + 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.display()); + + 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); +} + +// 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) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..e3c7e47 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,28 @@ +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] + +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); + +#[cfg(test)] +mod tests { + use super::*; + use std::ffi::CString; + + #[test] + fn two_plus_two() { + unsafe { + let interp = Tcl_CreateInterp(); + let script = CString::new("expr 2 + 2").unwrap(); + let eval_status = Tcl_Eval(interp, script.as_ptr()); + assert_eq!(eval_status, TCL_OK as i32); + let result = Tcl_GetObjResult(interp); + let mut result_value = 0; + let get_int_status = Tcl_GetIntFromObj(interp, result, &mut result_value); + assert_eq!(get_int_status, TCL_OK as i32); + assert_eq!(result_value, 4); + + Tcl_DeleteInterp(interp); + } + } +} diff --git a/wrapper.h b/wrapper.h new file mode 100644 index 0000000..5364b2e --- /dev/null +++ b/wrapper.h @@ -0,0 +1 @@ +#include <tcl.h> |