use std::collections::HashSet; use std::io::Write; use std::os::unix::io::{AsRawFd, FromRawFd}; use std::panic::Location; use std::path::Path; use std::process::Command; use std::process::Stdio; // See also: https://doc.rust-lang.org/cargo/reference/build-scripts.html fn main() { let jobserver = unsafe { jobserver::Client::from_env() }; let jobserver = jobserver.as_ref(); // The file structure roughly looks as follows: // . // ├── build.rs // you are here // ├── src/ // ├── tarantool-sys // │ ├── CMakeLists.txt // used for dynamic build // │ └── static-build // │ └── CMakeLists.txt // configures above CMakeLists.txt for static build // ├── picodata-webui // └── <target-dir>/<build-type>/build // <- build_root // ├── picodata-<smth>/out // <- std::env::var("OUT_DIR") // ├── picodata-webui // ├── tarantool-http // └── tarantool-sys/{static,dynamic} // ├── ncurses-prefix // ├── openssl-prefix // ├── readline-prefix // └── tarantool-prefix // let out_dir = std::env::var("OUT_DIR").unwrap(); dbg!(&out_dir); // "<target-dir>/<build-type>/build/picodata-<smth>/out" // Running `cargo build` and `cargo clippy` produces 2 different // `out_dir` paths. To avoid unnecessary rebuilds we use a different // build root for foreign deps (tarantool-sys, tarantool-http, // picodata-webui) let build_root = Path::new(&out_dir).ancestors().nth(2).unwrap(); dbg!(&build_root); // "<target-dir>/<build-type>/build" // See also: // https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts if let Some(ref makeflags) = std::env::var_os("CARGO_MAKEFLAGS") { std::env::set_var("MAKEFLAGS", makeflags); } #[allow(clippy::print_literal)] for (var, value) in std::env::vars() { println!("[{}:{}] {var}={value}", file!(), line!()); } set_git_describe_env_var(); // This variable controls the type of the build for whole project. // By default static linking is used (see tarantool-sys/static-build) // If feature dynamic_build is set we build tarantool using root cmake project // For details on how this passed to build.rs see: // https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts let use_static_build = std::env::var("CARGO_FEATURE_DYNAMIC_BUILD").is_err(); generate_export_stubs(&out_dir); build_tarantool(jobserver, build_root, use_static_build); build_http(jobserver, build_root, use_static_build); #[cfg(feature = "webui")] build_webui(build_root); println!("cargo:rerun-if-changed=tarantool-sys"); println!("cargo:rerun-if-changed=http/http"); #[cfg(feature = "webui")] rerun_if_webui_changed(); } fn set_git_describe_env_var() { if std::env::var("GIT_DESCRIBE").is_ok() { return; } let output = Command::new("git").arg("describe").output().unwrap(); assert!( output.status.success(), "`git describe` failed: {}{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr), ); let git_describe = std::str::from_utf8(&output.stdout).unwrap(); println!("cargo:rustc-env=GIT_DESCRIBE={git_describe}"); // We use output from `git describe` to generate picodata version info, so // we must rerun this build script every time that output changes. We can't // express this requirement explicitly using cargo's commands, but we can // ask cargo to rerun every time one of the important .git/* files changes, // which seems to do the trick for the most part. println!("cargo:rerun-if-changed=.git/HEAD"); println!("cargo:rerun-if-changed=.git/refs"); } fn generate_export_stubs(out_dir: &str) { let mut symbols = HashSet::with_capacity(1024); let exports = std::fs::read_to_string("tarantool-sys/extra/exports").unwrap(); read_symbols_into(&exports, &mut symbols); let exports = std::fs::read_to_string("tarantool-sys/extra/exports_libcurl").unwrap(); read_symbols_into(&exports, &mut symbols); let mut code = Vec::with_capacity(2048); writeln!(code, "pub fn export_symbols() {{").unwrap(); writeln!(code, " extern \"C\" {{").unwrap(); for symbol in &symbols { writeln!(code, " static {symbol}: *const ();").unwrap(); } writeln!(code, " }}").unwrap(); // TODO: use std::hint::black_box, when we move to rust 1.66 writeln!(code, " fn black_box(_: *const ()) {{}}").unwrap(); writeln!(code, " unsafe {{").unwrap(); for symbol in &symbols { writeln!(code, " black_box({symbol});").unwrap(); } writeln!(code, " }}").unwrap(); writeln!(code, "}}").unwrap(); let gen_path = std::path::Path::new(out_dir).join("export_symbols.rs"); std::fs::write(gen_path, code).unwrap(); fn read_symbols_into<'a>(file_contents: &'a str, symbols: &mut HashSet<&'a str>) { for line in file_contents.lines() { let line = line.trim(); if line.starts_with('#') { continue; } if line.is_empty() { continue; } symbols.insert(line); } } } #[cfg(feature = "webui")] fn rerun_if_webui_changed() { use std::fs; let source_dir = std::env::current_dir().unwrap().join("picodata-webui"); // Do not rerun for generated files changes let ignored_files = ["node_modules", ".husky"]; for entry in fs::read_dir(source_dir) .expect("failed to scan picodata-webui dir") .flatten() { if !ignored_files.contains(&entry.file_name().to_str().unwrap()) { println!( "cargo:rerun-if-changed=picodata-webui/{}", entry.file_name().to_string_lossy() ); } } } #[cfg(feature = "webui")] fn build_webui(build_root: &Path) { if std::env::var("WEBUI_BUNDLE").is_ok() { println!("building webui_bundle skipped"); return; } println!("building webui_bundle ..."); let source_dir = std::env::current_dir().unwrap().join("picodata-webui"); let out_dir = build_root.join("picodata-webui"); let out_dir_str = out_dir.display().to_string(); let webui_bundle = out_dir.join("bundle.json"); println!("cargo:rustc-env=WEBUI_BUNDLE={}", webui_bundle.display()); Command::new("yarn") .arg("install") .arg("--prefer-offline") .arg("--frozen-lockfile") .arg("--no-progress") .arg("--non-interactive") .current_dir(&source_dir) .run(); Command::new("yarn") .arg("vite") .arg("build") .args(["--outDir", &out_dir_str]) .arg("--emptyOutDir") .current_dir(&source_dir) .run(); } const TARANTOOL_SYS_STATIC: &str = "tarantool-sys/static"; const TARANTOOL_SYS_DYNAMIC: &str = "tarantool-sys/dynamic"; fn build_http(jsc: Option<&jobserver::Client>, build_root: &Path, use_static_build: bool) { let build_dir = build_root.join("tarantool-http"); let build_dir_str = build_dir.display().to_string(); let tarantool_dir = if use_static_build { build_root .join(TARANTOOL_SYS_STATIC) .join("tarantool-prefix") } else { build_root.join(TARANTOOL_SYS_DYNAMIC) }; Command::new("cmake") .args(["-S", "http"]) .args(["-B", &build_dir_str]) .arg(format!("-DTARANTOOL_DIR={}", tarantool_dir.display())) .run(); let mut cmd = Command::new("cmake"); cmd.args(["--build", &build_dir_str]); if let Some(jsc) = jsc { jsc.configure(&mut cmd); } cmd.run(); Command::new("ar") .arg("-rcs") .arg(build_dir.join("libhttpd.a")) .arg(build_dir.join("http/CMakeFiles/httpd.dir/lib.c.o")) .run(); rustc::link_search(build_dir_str); } fn build_tarantool(jsc: Option<&jobserver::Client>, build_root: &Path, use_static_build: bool) { let tarantool_sys = if use_static_build { build_root.join(TARANTOOL_SYS_STATIC) } else { build_root.join(TARANTOOL_SYS_DYNAMIC) }; let tarantool_build = if use_static_build { tarantool_sys.join("tarantool-prefix/src/tarantool-build") } else { tarantool_sys.clone() }; if !tarantool_build.exists() { // Build from scratch let mut configure_cmd = Command::new("cmake"); configure_cmd.arg("-B").arg(&tarantool_sys); let common_args = [ "-DCMAKE_BUILD_TYPE=RelWithDebInfo", "-DBUILD_TESTING=FALSE", "-DBUILD_DOC=FALSE", ]; if use_static_build { // static build is a separate project that uses CMAKE_TARANTOOL_ARGS // to forward parameters to tarantool cmake project configure_cmd .args(["-S", "tarantool-sys/static-build"]) .arg(format!("-DCMAKE_TARANTOOL_ARGS={}", &common_args.join(";"))) } else { // for dynamic build we do not use most of the bundled dependencies configure_cmd .args(["-S", "tarantool-sys"]) .args(common_args) .args([ "-DENABLE_BUNDLED_LDAP=OFF", "-DENABLE_BUNDLED_ZSTD=OFF", "-DENABLE_BUNDLED_LIBCURL=OFF", "-DENABLE_BUNDLED_LIBYAML=OFF", ]) // for dynamic build we'll also need to install the project, so configure the prefix .arg(format!( "-DCMAKE_INSTALL_PREFIX={}", tarantool_sys.display(), )) } .run(); } let mut build_cmd = Command::new("cmake"); build_cmd.arg("--build").arg(&tarantool_sys); if !use_static_build { build_cmd.args(["--", "install"]); } if let Some(jsc) = jsc { jsc.configure(&mut build_cmd); } build_cmd.run(); let tarantool_sys = tarantool_sys.display(); let tarantool_build = tarantool_build.display(); // Don't build a shared object in case it's the default for the compiler rustc::link_arg("-no-pie"); for l in [ "core", "small", "msgpuck", "vclock", "bit", "swim", "uri", "json", "http_parser", "mpstream", "raft", "csv", "bitset", "coll", "fakesys", "salad", "tzcode", ] { rustc::link_search(format!("{tarantool_build}/src/lib/{l}")); rustc::link_lib_static(l); } rustc::link_search(format!("{tarantool_build}/src/lib/crypto")); rustc::link_lib_static("tcrypto"); rustc::link_search(format!("{tarantool_build}")); rustc::link_search(format!("{tarantool_build}/src")); rustc::link_search(format!("{tarantool_build}/src/box")); rustc::link_search(format!("{tarantool_build}/third_party/c-dt/build")); rustc::link_search(format!("{tarantool_build}/third_party/luajit/src")); if use_static_build { rustc::link_search(format!("{tarantool_build}/build/libyaml/lib")); rustc::link_search(format!("{tarantool_build}/build/nghttp2/dest/lib")); } rustc::link_lib_static("tarantool"); if use_static_build { rustc::link_lib_static("ev") } else { rustc::link_lib_dynamic("ev"); } rustc::link_lib_static("coro"); rustc::link_lib_static("cdt"); rustc::link_lib_static("server"); rustc::link_lib_static("misc"); rustc::link_lib_static("decNumber"); rustc::link_lib_static("eio"); rustc::link_lib_static("box"); rustc::link_lib_static("tuple"); rustc::link_lib_static("xrow"); rustc::link_lib_static("box_error"); rustc::link_lib_static("xlog"); rustc::link_lib_static("crc32"); rustc::link_lib_static("stat"); rustc::link_lib_static("shutdown"); rustc::link_lib_static("swim_udp"); rustc::link_lib_static("swim_ev"); rustc::link_lib_static("symbols"); rustc::link_lib_static("cpu_feature"); rustc::link_lib_static("luajit"); rustc::link_lib_static("xxhash"); if use_static_build { rustc::link_lib_static("nghttp2"); rustc::link_lib_static("zstd"); rustc::link_lib_static("yaml_static"); } else { rustc::link_lib_dynamic("yaml"); rustc::link_lib_dynamic("zstd"); } // Add LDAP authentication support libraries. if use_static_build { rustc::link_search(format!("{tarantool_build}/bundled-ldap-prefix/lib")); rustc::link_lib_static_no_whole_archive("ldap"); rustc::link_lib_static_no_whole_archive("lber"); rustc::link_search(format!("{tarantool_build}/bundled-sasl-prefix/lib")); rustc::link_lib_static_no_whole_archive("sasl2"); } else { rustc::link_lib_dynamic("sasl2"); rustc::link_lib_dynamic("ldap"); } if cfg!(target_os = "macos") { // Currently we link against 2 versions of `decNumber` library: one // comes with tarantool and ther other comes from the `dec` cargo crate. // On macos this seems to confuse the linker, which just chooses one of // the libraries and complains that it can't find symbols from the other. // So we add the second library explicitly via full path to the file. rustc::link_arg(format!("{tarantool_build}/libdecNumber.a")); // Libunwind is built into the compiler on macos } else { // These two must be linked as positional arguments, because they define // duplicate symbols which is not allowed (by default) when linking with // via -l... option let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); let lib_dir = format!("{tarantool_build}/third_party/libunwind/src/.libs"); rustc::link_arg(format!("{lib_dir}/libunwind-{arch}.a")); rustc::link_arg(format!("{lib_dir}/libunwind.a")); } rustc::link_arg("-lc"); if use_static_build { rustc::link_search(format!("{tarantool_sys}/readline-prefix/lib")); rustc::link_lib_static("readline"); rustc::link_search(format!("{tarantool_sys}/icu-prefix/lib")); rustc::link_lib_static("icudata"); rustc::link_lib_static("icui18n"); rustc::link_lib_static("icuio"); rustc::link_lib_static("icutu"); rustc::link_lib_static("icuuc"); // "z" is linked with curl on cmake stage in case of a dynamic build rustc::link_search(format!("{tarantool_sys}/zlib-prefix/lib")); rustc::link_lib_static("z"); rustc::link_search(format!("{tarantool_build}/build/curl/dest/lib")); rustc::link_lib_static("curl"); // c-ares not used with dynamic build because we have curl-openssl-dev rustc::link_search(format!("{tarantool_build}/build/ares/dest/lib")); rustc::link_lib_dynamic("cares"); rustc::link_search(format!("{tarantool_sys}/openssl-prefix/lib")); rustc::link_lib_static("ssl"); rustc::link_lib_static("crypto"); rustc::link_search(format!("{tarantool_sys}/ncurses-prefix/lib")); rustc::link_lib_static("tinfo"); } else { if cfg!(target_os = "macos") { // On macos icu4c and readline are keg-only, which means they were not // symlinked into /usr/local. We should add the search path manually. rustc::link_search("/usr/local/opt/icu4c/lib"); rustc::link_search("/usr/local/opt/readline/lib"); } rustc::link_lib_dynamic("readline"); rustc::link_lib_dynamic("icudata"); rustc::link_lib_dynamic("icui18n"); rustc::link_lib_dynamic("icuio"); rustc::link_lib_dynamic("icutu"); rustc::link_lib_dynamic("icuuc"); rustc::link_lib_dynamic("curl"); rustc::link_lib_dynamic("ssl"); rustc::link_lib_dynamic("crypto"); } rustc::link_search(format!("{tarantool_sys}/iconv-prefix/lib")); if cfg!(target_os = "macos") { // -lc++ instead of -lstdc++ on macos rustc::link_lib_dynamic("c++"); // -lresolv on macos rustc::link_lib_dynamic("resolv"); } else { // not supported on macos // We use -rdynamic instead of -export-dynamic // because on fedora this likely triggers this bug // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47390 rustc::link_arg("-rdynamic"); rustc::link_lib_dynamic("stdc++"); } } trait CommandExt { fn run(&mut self); } impl CommandExt for Command { #[track_caller] fn run(&mut self) { let loc = Location::caller(); println!("[{}:{}] running [{:?}]", loc.file(), loc.line(), self); // Redirect stderr to stdout. This is needed to preserve the order of // error messages in the output, because otherwise cargo separates the // streams into 2 chunks destroying any hope of understanding when the // errors happened. let stdout_fd = std::io::stdout().as_raw_fd(); let stdout_dup_fd = nix::unistd::dup(stdout_fd).expect("what could go wrong?"); let stdout = unsafe { Stdio::from_raw_fd(stdout_dup_fd) }; self.stderr(stdout); let prog = self.get_program().to_owned().into_string().unwrap(); match self.status() { Ok(status) if status.success() => (), Ok(status) => panic!("{} failed: {}", prog, status), Err(e) => panic!("failed running `{}`: {}", prog, e), } } } mod rustc { pub fn link_search(path: impl AsRef<str>) { println!("cargo:rustc-link-search=native={}", path.as_ref()); } pub fn link_lib_static(lib: impl AsRef<str>) { // See also: // https://doc.rust-lang.org/cargo/reference/build-scripts.html#cargorustc-link-liblib // https://doc.rust-lang.org/rustc/command-line-arguments.html#option-l-link-lib println!( "cargo:rustc-link-lib=static:+whole-archive,-bundle={}", lib.as_ref() ); } // NB: this is needed less often and thus designed to be opt-out. pub fn link_lib_static_no_whole_archive(lib: impl AsRef<str>) { println!("cargo:rustc-link-lib=static:-bundle={}", lib.as_ref()); } pub fn link_lib_dynamic(lib: impl AsRef<str>) { println!("cargo:rustc-link-lib=dylib={}", lib.as_ref()); } pub fn link_arg(arg: impl AsRef<str>) { println!("cargo:rustc-link-arg={}", arg.as_ref()); } }