From 7f0f5dad2458313e2800cf915c55f180184f6617 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Tue, 11 Sep 2018 18:33:46 -0700 Subject: [PATCH] Update to build curl from scratch It looks like it's basically not possible for MSVC to pick up nghttp2 otherwise because that detection requires `pkg-config`. I'm sort of personally real tired of dealing with build systems, so instead use the `cc` crate to do everything. This commit switches builds from source to always use the `cc` crate and we manually list all the relevant `#define` directives as well as the list of files that we're interested in. This is likely not reproducing exactly what libcurl requires 100%, but it's providing a lot of other benefits: * This build strategy truly will be cross platform modulo us fixing bugs. We can now actually get dependency detection and such working across all platforms as we don't rely on external tools. * We can now use the vanilla upstream `curl` submodule because there's no need to generate a configure script. * The `curl` CLI tool is no longer built, we never needed it anyway! * We have much more precise control about what's happening in that we can precisely and programmatically know what's activated in the build. Downsides of this approach include: * Updates of the `curl` submodule are likely going to be harder as we need to make sure all the right files are built and they're built with the right `#define`s. This is expected to be a very low cost over time compared to the headache dealing with cross-platform builds right now. * We likely aren't building `curl` in the exact same way, for example some `#define` might be required to make curl "go fast" on newer platforms, but we may forget to pass it until much later. This is again, however, seen as not much of a problem compared to the current headache trying to build curl in a cross-platform manner that hooks up all the dependencies correctly. --- .gitmodules | 2 +- curl-sys/build.rs | 558 +++++++++++++++++++--------------------------- curl-sys/curl | 2 +- 3 files changed, 234 insertions(+), 328 deletions(-) diff --git a/.gitmodules b/.gitmodules index 7196785ddd..b69d1a5865 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "curl-sys/curl"] path = curl-sys/curl - url = https://github.com/alexcrichton/curl + url = https://github.com/curl/curl diff --git a/curl-sys/build.rs b/curl-sys/build.rs index e9f1894f23..687889f368 100644 --- a/curl-sys/build.rs +++ b/curl-sys/build.rs @@ -3,27 +3,13 @@ extern crate pkg_config; extern crate vcpkg; extern crate cc; -#[allow(unused_imports, deprecated)] -use std::ascii::AsciiExt; use std::env; -use std::ffi::OsString; use std::fs; -use std::path::{PathBuf, Path, Component, Prefix}; +use std::path::{PathBuf, Path}; use std::process::Command; -use std::io::ErrorKind; - -macro_rules! t { - ($e:expr) => (match $e { - Ok(t) => t, - Err(e) => panic!("{} return the error {}", stringify!($e), e), - }) -} fn main() { let target = env::var("TARGET").unwrap(); - let host = env::var("HOST").unwrap(); - let src = env::current_dir().unwrap(); - let dst = PathBuf::from(env::var_os("OUT_DIR").unwrap()); let windows = target.contains("windows"); // OSX and Haiku ships libcurl by default, so we just use that version @@ -32,27 +18,15 @@ fn main() { return println!("cargo:rustc-flags=-l curl"); } - // Illumos/Solaris requires explicit linking with libnsl - if target.contains("solaris") { - println!("cargo:rustc-flags=-l nsl"); - } - // Next, fall back and try to use pkg-config if its available. - if !target.contains("windows") { - match pkg_config::find_library("libcurl") { - Ok(lib) => { - for path in lib.include_paths.iter() { - println!("cargo:include={}", path.display()); - } - return - } - Err(e) => println!("Couldn't find libcurl from \ - pkgconfig ({:?}), compiling it from source...", e), + if windows { + if try_vcpkg() { + return + } + } else { + if try_pkg_config() { + return } - } - - if try_vcpkg() { - return; } if !Path::new("curl/.git").exists() { @@ -60,304 +34,201 @@ fn main() { .status(); } - println!("cargo:rustc-link-search=native={}/lib", dst.display()); - println!("cargo:rustc-link-lib=static=curl"); + let dst = PathBuf::from(env::var_os("OUT_DIR").unwrap()); + let include = dst.join("include"); + let build = dst.join("build"); println!("cargo:root={}", dst.display()); - println!("cargo:include={}/include", dst.display()); - if windows { - println!("cargo:rustc-link-lib=ws2_32"); - println!("cargo:rustc-link-lib=crypt32"); - } - - // MSVC builds are just totally different - if target.contains("msvc") { - return build_msvc(&target); - } - - let openssl_root = register_dep("OPENSSL"); - let zlib_root = register_dep("Z"); - let nghttp2_root = register_dep("NGHTTP2"); - - let cfg = cc::Build::new(); - let compiler = cfg.get_compiler(); - - let _ = fs::create_dir(&dst.join("build")); - - let mut cmd = Command::new("sh"); - let mut cflags = OsString::new(); - for arg in compiler.args() { - cflags.push(arg); - cflags.push(" "); + println!("cargo:include={}", include.display()); + fs::create_dir_all(include.join("curl")).unwrap(); + fs::copy("curl/include/curl/curl.h", include.join("curl/curl.h")).unwrap(); + fs::copy("curl/include/curl/curlver.h", include.join("curl/curlver.h")).unwrap(); + fs::copy("curl/include/curl/easy.h", include.join("curl/easy.h")).unwrap(); + fs::copy("curl/include/curl/mprintf.h", include.join("curl/mprintf.h")).unwrap(); + fs::copy("curl/include/curl/multi.h", include.join("curl/multi.h")).unwrap(); + fs::copy("curl/include/curl/stdcheaders.h", include.join("curl/stdcheaders.h")).unwrap(); + fs::copy("curl/include/curl/system.h", include.join("curl/system.h")).unwrap(); + fs::copy("curl/include/curl/typecheck-gcc.h", include.join("curl/typecheck-gcc.h")).unwrap(); + let mut cfg = cc::Build::new(); + cfg.out_dir(&build) + .include("curl/lib") + .include("curl/include") + .define("BUILDING_LIBCURL", None) + .define("CURL_DISABLE_CRYPTO_AUTH", None) + .define("CURL_DISABLE_DICT", None) + .define("CURL_DISABLE_FTP", None) + .define("CURL_DISABLE_GOPHER", None) + .define("CURL_DISABLE_IMAP", None) + .define("CURL_DISABLE_LDAP", None) + .define("CURL_DISABLE_LDAPS", None) + .define("CURL_DISABLE_NTLM", None) + .define("CURL_DISABLE_POP3", None) + .define("CURL_DISABLE_RTSP", None) + .define("CURL_DISABLE_SMB", None) + .define("CURL_DISABLE_SMTP", None) + .define("CURL_DISABLE_TELNET", None) + .define("CURL_DISABLE_TFTP", None) + .define("CURL_STATICLIB", None) + .define("ENABLE_IPV6", None) + .define("HAVE_ASSERT_H", None) + .define("OS", "\"unknown\"") // TODO + .define("HAVE_ZLIB_H", None) + .define("HAVE_LIBZ", None) + + .file("curl/lib/asyn-thread.c") + .file("curl/lib/base64.c") + .file("curl/lib/conncache.c") + .file("curl/lib/connect.c") + .file("curl/lib/content_encoding.c") + .file("curl/lib/cookie.c") + .file("curl/lib/curl_addrinfo.c") + .file("curl/lib/curl_ctype.c") + .file("curl/lib/curl_memrchr.c") + .file("curl/lib/curl_range.c") + .file("curl/lib/curl_threads.c") + .file("curl/lib/dotdot.c") + .file("curl/lib/easy.c") + .file("curl/lib/escape.c") + .file("curl/lib/file.c") + .file("curl/lib/fileinfo.c") + .file("curl/lib/formdata.c") + .file("curl/lib/getenv.c") + .file("curl/lib/getinfo.c") + .file("curl/lib/hash.c") + .file("curl/lib/hostasyn.c") + .file("curl/lib/hostcheck.c") + .file("curl/lib/hostip.c") + .file("curl/lib/hostip6.c") + .file("curl/lib/http.c") + .file("curl/lib/http2.c") + .file("curl/lib/http_chunks.c") + .file("curl/lib/http_proxy.c") + .file("curl/lib/if2ip.c") + .file("curl/lib/inet_ntop.c") + .file("curl/lib/inet_pton.c") + .file("curl/lib/llist.c") + .file("curl/lib/mime.c") + .file("curl/lib/mprintf.c") + .file("curl/lib/multi.c") + .file("curl/lib/netrc.c") + .file("curl/lib/nonblock.c") + .file("curl/lib/parsedate.c") + .file("curl/lib/pipeline.c") + .file("curl/lib/progress.c") + .file("curl/lib/rand.c") + .file("curl/lib/select.c") + .file("curl/lib/sendf.c") + .file("curl/lib/setopt.c") + .file("curl/lib/share.c") + .file("curl/lib/slist.c") + .file("curl/lib/socks.c") + .file("curl/lib/speedcheck.c") + .file("curl/lib/splay.c") + .file("curl/lib/strcase.c") + .file("curl/lib/strdup.c") + .file("curl/lib/strerror.c") + .file("curl/lib/strtok.c") + .file("curl/lib/strtoofft.c") + .file("curl/lib/timeval.c") + .file("curl/lib/transfer.c") + .file("curl/lib/url.c") + .file("curl/lib/version.c") + .file("curl/lib/vtls/vtls.c") + .file("curl/lib/warnless.c") + .file("curl/lib/wildcard.c") + + .define("HAVE_GETADDRINFO", None) + + .warnings(false); + + if cfg!(feature = "http2") { + cfg.define("USE_NGHTTP2", None) + .define("NGHTTP2_STATICLIB", None); + + if let Some(path) = env::var_os("DEP_NGHTTP2_ROOT") { + let path = PathBuf::from(path); + cfg.include(path.join("include")); + } } - // Can't run ./configure directly on msys2 b/c we're handing in - // Windows-style paths (those starting with C:\), but it chokes on those. - // For that reason we build up a shell script with paths converted to - // posix versions hopefully... - // - // Also apparently the buildbots choke unless we manually set LD, who knows - // why?! - cmd.env("CC", compiler.path()) - .env("CFLAGS", cflags) - .env("LD", &which("ld").unwrap()) - .env("VERBOSE", "1") - .current_dir(&dst.join("build")) - .arg(msys_compatible(&src.join("curl/configure"))); - - // For now this build script doesn't support paths with spaces in them. This - // is arguably a but in curl's configure script, but we could also try to - // paper over it by using a tmp directory which *doesn't* have spaces in it. - // As of now though that's not implemented so just give a nicer error for - // the time being. - let wants_space_error = windows && - (dst.to_str().map(|s| s.contains(" ")).unwrap_or(false) || - src.to_str().map(|s| s.contains(" ")).unwrap_or(false)); - if wants_space_error { - panic!("\n\nunfortunately ./configure of libcurl is known to \ - fail if there's a space in the path to the current \ - directory\n\n\ - there's a space in either\n {}\n {}\nand this will cause the \ - build to fail\n\n\ - the MSVC build should work with a directory that has \ - spaces in it, and it would also work to move this to a \ - different directory without spaces\n\n", - src.display(), dst.display()) + if let Some(path) = env::var_os("DEP_Z_INCLUDE") { + cfg.include(path); } if windows { - cmd.arg("--with-winssl"); + cfg.define("USE_THREADS_WIN32", None) + .define("HAVE_IOCTLSOCKET_FIONBIO", None) + .define("USE_WINSOCK", None) + .define("USE_WINDOWS_SSPI", None) + .define("USE_SCHANNEL", None) + .file("curl/lib/x509asn1.c") + .file("curl/lib/curl_sspi.c") + .file("curl/lib/socks_sspi.c") + .file("curl/lib/system_win32.c") + .file("curl/lib/vtls/schannel.c") + .file("curl/lib/vtls/schannel_verify.c"); } else { - cmd.arg("--without-ca-bundle"); - cmd.arg("--without-ca-path"); - } - if let Some(root) = openssl_root { - cmd.arg(format!("--with-ssl={}", msys_compatible(&root))); - } - if let Some(root) = zlib_root { - cmd.arg(format!("--with-zlib={}", msys_compatible(&root))); - } - cmd.arg("--enable-static=yes"); - cmd.arg("--enable-shared=no"); - match &env::var("PROFILE").unwrap()[..] { - "bench" | "release" => { - cmd.arg("--enable-optimize"); + cfg.define("RECV_TYPE_ARG1", "int") + .define("HAVE_PTHREAD_H", None) + .define("HAVE_ARPA_INET_H", None) + .define("HAVE_ERRNO_H", None) + .define("HAVE_FCNTL_H", None) + .define("HAVE_NETDB_H", None) + .define("HAVE_NETINET_IN_H", None) + .define("HAVE_POLL_FINE", None) + .define("HAVE_POLL_H", None) + .define("HAVE_FCNTL_O_NONBLOCK", None) + .define("HAVE_SYS_SELECT_H", None) + .define("HAVE_SYS_STAT_H", None) + .define("HAVE_UNISTD_H", None) + .define("HAVE_RECV", None) + .define("HAVE_SELECT", None) + .define("HAVE_SEND", None) + .define("HAVE_SOCKET", None) + .define("HAVE_STERRROR_R", None) + .define("HAVE_STRUCT_TIMEVAL", None) + .define("USE_THREADS_POSIX", None) + .define("USE_OPENSSL", None) + .define("RECV_TYPE_ARG2", "void*") + .define("RECV_TYPE_ARG3", "size_t") + .define("RECV_TYPE_ARG4", "int") + .define("RECV_TYPE_RETV", "ssize_t") + .define("SEND_QUAL_ARG2", "const") + .define("SEND_TYPE_ARG1", "int") + .define("SEND_TYPE_ARG2", "void*") + .define("SEND_TYPE_ARG3", "size_t") + .define("SEND_TYPE_ARG4", "int") + .define("SEND_TYPE_RETV", "ssize_t") + .define("SIZEOF_CURL_OFF_T", "8") + .define("SIZEOF_INT", "4") + .define("SIZEOF_SHORT", "2") + .file("curl/lib/vtls/openssl.c"); + + let width = env::var("CARGO_CFG_TARGET_POINTER_WIDTH") + .unwrap() + .parse::() + .unwrap(); + cfg.define("SIZEOF_SSIZE_T", Some(&(width / 8).to_string()[..])); + cfg.define("SIZEOF_SIZE_T", Some(&(width / 8).to_string()[..])); + cfg.define("SIZEOF_LONG", Some(&(width / 8).to_string()[..])); + + if let Some(path) = env::var_os("DEP_OPENSSL_INCLUDE") { + cfg.include(path); } - _ => { - cmd.arg("--enable-debug"); - cmd.arg("--disable-optimize"); - } - } - cmd.arg(format!("--prefix={}", msys_compatible(&dst))); - - if target != host && - (!target.contains("windows") || !host.contains("windows")) { - // NOTE GNU terminology - // BUILD = machine where we are (cross) compiling curl - // HOST = machine where the compiled curl will be used - // TARGET = only relevant when compiling compilers - if target.contains("windows") { - // curl's configure can't parse `-windows-` triples when used - // as `--host`s. In those cases we use this combination of - // `host` and `target` that appears to do the right thing. - cmd.arg(format!("--host={}", host)); - cmd.arg(format!("--target={}", target)); - } else { - cmd.arg(format!("--build={}", host)); - cmd.arg(format!("--host={}", target)); - } - } - if let Some(root) = nghttp2_root { - cmd.arg(format!("--with-nghttp2={}", msys_compatible(&root))); - } else { - cmd.arg("--without-nghttp2"); + cfg.flag("-fvisibility=hidden"); } - cmd.arg("--without-librtmp"); - cmd.arg("--without-libidn2"); - cmd.arg("--without-libssh2"); - cmd.arg("--without-libpsl"); - cmd.arg("--disable-ldap"); - cmd.arg("--disable-ldaps"); - cmd.arg("--disable-ftp"); - cmd.arg("--disable-rtsp"); - cmd.arg("--disable-dict"); - cmd.arg("--disable-telnet"); - cmd.arg("--disable-tftp"); - cmd.arg("--disable-pop3"); - cmd.arg("--disable-imap"); - cmd.arg("--disable-smtp"); - cmd.arg("--disable-gopher"); - cmd.arg("--disable-manual"); - cmd.arg("--disable-smb"); - cmd.arg("--disable-sspi"); - cmd.arg("--disable-manual"); - cmd.arg("--disable-unix-sockets"); - cmd.arg("--disable-versioned-symbols"); - cmd.arg("--enable-hidden-symbols"); - cmd.arg("--disable-libcurl-option"); - - run(&mut cmd, "sh"); - run(make() - .arg(&format!("-j{}", env::var("NUM_JOBS").unwrap())) - .current_dir(&dst.join("build")), "make"); - run(make() - .arg("install") - .current_dir(&dst.join("build")), "make"); -} + cfg.compile("curl"); -fn run(cmd: &mut Command, program: &str) { - println!("running: {:?}", cmd); - let status = match cmd.status() { - Ok(status) => status, - Err(ref e) if e.kind() == ErrorKind::NotFound => { - fail(&format!("failed to execute command: {}\nis `{}` not installed?", - e, program)); - } - Err(e) => fail(&format!("failed to execute command: {}", e)), - }; - if !status.success() { - fail(&format!("command did not execute successfully, got: {}", status)); - } -} - -fn fail(s: &str) -> ! { - panic!("\n{}\n\nbuild script failed, must exit now", s) -} - -fn make() -> Command { - let cmd = if cfg!(target_os = "freebsd") {"gmake"} else {"make"}; - let mut cmd = Command::new(cmd); - // We're using the MSYS make which doesn't work with the mingw32-make-style - // MAKEFLAGS, so remove that from the env if present. - if cfg!(windows) { - cmd.env_remove("MAKEFLAGS").env_remove("MFLAGS"); - } - return cmd -} - -fn which(cmd: &str) -> Option { - let cmd = format!("{}{}", cmd, env::consts::EXE_SUFFIX); - let paths = env::var_os("PATH").unwrap(); - env::split_paths(&paths).map(|p| p.join(&cmd)).find(|p| { - fs::metadata(p).is_ok() - }) -} - -fn msys_compatible(path: &Path) -> String { - let mut path_string = path.to_str().unwrap().to_string(); - if !cfg!(windows) { - return path_string; - } - - // Replace e.g. C:\ with /c/ - if let Component::Prefix(prefix_component) = path.components().next().unwrap() { - if let Prefix::Disk(disk) = prefix_component.kind() { - let from = format!("{}:\\", disk as char); - let to = format!("/{}/", (disk as char).to_ascii_lowercase()); - path_string = path_string.replace(&from, &to); - } - } - path_string.replace("\\", "/") -} - -fn register_dep(dep: &str) -> Option { - if let Some(s) = env::var_os(&format!("DEP_{}_ROOT", dep)) { - prepend("PKG_CONFIG_PATH", Path::new(&s).join("lib/pkgconfig")); - return Some(s.into()) - } - if let Some(s) = env::var_os(&format!("DEP_{}_INCLUDE", dep)) { - let root = Path::new(&s).parent().unwrap(); - env::set_var(&format!("DEP_{}_ROOT", dep), root); - let path = root.join("lib/pkgconfig"); - if path.exists() { - prepend("PKG_CONFIG_PATH", path); - return Some(root.to_path_buf()) - } - } - - return None; - - fn prepend(var: &str, val: PathBuf) { - let prefix = env::var(var).unwrap_or(String::new()); - let mut v = vec![val]; - v.extend(env::split_paths(&prefix)); - env::set_var(var, &env::join_paths(v).unwrap()); - } -} - -fn build_msvc(target: &str) { - let cmd = cc::windows_registry::find(target, "nmake.exe"); - let mut cmd = cmd.unwrap_or(Command::new("nmake.exe")); - let src = env::current_dir().unwrap(); - let dst = PathBuf::from(env::var_os("OUT_DIR").unwrap()); - let machine = if target.starts_with("x86_64") { - "x64" - } else if target.starts_with("i686") { - "x86" - } else { - panic!("unknown msvc target: {}", target); - }; - - t!(fs::create_dir_all(dst.join("include/curl"))); - t!(fs::create_dir_all(dst.join("lib"))); - - drop(fs::remove_dir_all(&dst.join("build"))); - cp_r(&src.join("curl"), &dst.join("build")); - cmd.current_dir(dst.join("build/winbuild")); - cmd.arg("/f").arg("Makefile.vc") - .arg("MODE=static") - .arg("ENABLE_IDN=yes") - .arg("DEBUG=no") - .arg("GEN_PDB=no") - .arg("ENABLE_WINSSL=yes") - .arg("ENABLE_SSPI=yes") - .arg(format!("MACHINE={}", machine)); - - // These env vars are intended for `make` usually, not nmake, so remove them - // unconditionally - cmd.env_remove("MAKEFLAGS") - .env_remove("MFLAGS"); - - // While in theory clang-cl can be used it doesn't work because we can't - // configure CFLAGS which means cross-compilation to 32-bit doesn't work. - // Just require MSVC cl.exe here. - cmd.env_remove("CC"); - - let features = env::var("CARGO_CFG_TARGET_FEATURE") - .unwrap_or(String::new()); - if features.contains("crt-static") { - cmd.arg("RTLIBCFG=static"); - } - - if let Some(inc) = env::var_os("DEP_Z_ROOT") { - let inc = PathBuf::from(inc); - let mut s = OsString::from("WITH_DEVEL="); - s.push(&inc); - cmd.arg("WITH_ZLIB=static").arg(s); - - // the build system for curl expects this library to be called - // zlib_a.lib, so make sure it's named correctly (where libz-sys just - // produces zlib.lib) - let _ = fs::remove_file(&inc.join("lib/zlib_a.lib")); - t!(fs::copy(inc.join("lib/zlib.lib"), inc.join("lib/zlib_a.lib"))); + if windows { + println!("cargo:rustc-link-lib=ws2_32"); + println!("cargo:rustc-link-lib=crypt32"); } - run(&mut cmd, "nmake"); - - let name = format!("libcurl-vc-{}-release-static-zlib-static-\ - ipv6-sspi-winssl", machine); - let libs = dst.join("build/builds").join(name); - t!(fs::copy(libs.join("lib/libcurl_a.lib"), dst.join("lib/curl.lib"))); - for f in t!(fs::read_dir(libs.join("include/curl"))) { - let path = t!(f).path(); - let dst = dst.join("include/curl").join(path.file_name().unwrap()); - t!(fs::copy(path, dst)); + // Illumos/Solaris requires explicit linking with libnsl + if target.contains("solaris") { + println!("cargo:rustc-link-lib=nsl"); } - t!(fs::remove_dir_all(dst.join("build/builds"))); - println!("cargo:rustc-link-lib=wldap32"); - println!("cargo:rustc-link-lib=advapi32"); - println!("cargo:rustc-link-lib=normaliz"); } #[cfg(not(target_env = "msvc"))] @@ -412,15 +283,50 @@ fn try_vcpkg() -> bool { false } -fn cp_r(src: &Path, dst: &Path) { - t!(fs::create_dir(dst)); - for e in t!(src.read_dir()).map(|e| t!(e)) { - let src = e.path(); - let dst = dst.join(e.file_name()); - if t!(e.file_type()).is_dir() { - cp_r(&src, &dst); - } else { - t!(fs::copy(&src, &dst)); +fn try_pkg_config() -> bool { + let mut cfg = pkg_config::Config::new(); + cfg.cargo_metadata(false); + let lib = match cfg.probe("libcurl") { + Ok(lib) => lib, + Err(e) => { + println!("Couldn't find libcurl from pkgconfig ({:?}), \ + compiling it from source...", e); + return false } + }; + + // Not all system builds of libcurl have http2 features enabled, so if we've + // got a http2-requested build then we may fall back to a build from source. + if cfg!(feature = "http2") { + let output = Command::new("curl-config") + .arg("--features") + .output(); + let output = match output { + Ok(out) => out, + Err(e) => { + println!("failed to run curl-config ({}), building from source", e); + return false + } + }; + if !output.status.success() { + println!("curl-config failed: {}", output.status); + return false + } + let stdout = String::from_utf8_lossy(&output.stdout); + if !stdout.contains("HTTP2") { + println!("failed to find http-2 feature enabled in pkg-config-found \ + libcurl, building from source"); + return false + } + } + + // Re-find the library to print cargo's metadata, then print some extra + // metadata as well. + cfg.cargo_metadata(true) + .probe("libcurl") + .unwrap(); + for path in lib.include_paths.iter() { + println!("cargo:include={}", path.display()); } + return true } diff --git a/curl-sys/curl b/curl-sys/curl index c88fd53b5b..432eb5f5c2 160000 --- a/curl-sys/curl +++ b/curl-sys/curl @@ -1 +1 @@ -Subproject commit c88fd53b5bb7fe8351ef602f1b2ec76559816f0c +Subproject commit 432eb5f5c254ee8383b2522ce597c9219877923e