diff --git a/Cargo.lock b/Cargo.lock index b45747d08ea4..7683c0e820cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5051,6 +5051,7 @@ dependencies = [ name = "wireguard-go-rs" version = "0.0.0" dependencies = [ + "anyhow", "log", "maybenot-ffi", "thiserror", diff --git a/wireguard-go-rs/Cargo.toml b/wireguard-go-rs/Cargo.toml index 63a5e462e178..062c80de34d2 100644 --- a/wireguard-go-rs/Cargo.toml +++ b/wireguard-go-rs/Cargo.toml @@ -4,13 +4,16 @@ description = "Rust bindings to wireguard-go with DAITA support" edition = "2021" license.workspace = true +[build-dependencies] +anyhow = "1.0" + [target.'cfg(unix)'.dependencies] thiserror.workspace = true log.workspace = true zeroize = "1.8.1" [target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies] -# The app does not depend on maybenot-ffi itself, but adds it as a dependency to expose FFI symbols to wireguard-go. +# The app does not depend on maybenot-ffi itself, but adds it as a dependency to expose FFI symbols to wireguard-go. # This is done, instead of using the makefile in wireguard-go to build maybenot-ffi into its archive, to prevent # name clashes induced by link-time optimization. # NOTE: the version of maybenot-ffi below must be the same as the version checked into the wireguard-go submodule diff --git a/wireguard-go-rs/build-wireguard-go.sh b/wireguard-go-rs/build-wireguard-go.sh deleted file mode 100755 index 35bb9413f20e..000000000000 --- a/wireguard-go-rs/build-wireguard-go.sh +++ /dev/null @@ -1,128 +0,0 @@ -#!/usr/bin/env bash -# -# This script is used to build libwg (wireguard-go as an FFI-friendly library) for different platforms. -# -# Supported arguments: -# * --android -# If libwg should be built for an Android target. -# * --daita -# Build libwg with "DAITA" support. -# - -set -eu - -# If the target OS is Android. -ANDROID="false" -# If Wireguard-go should be built with DAITA-support. -DAITA="false" - -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -BUILD_DIR="$SCRIPT_DIR/../build" - -while [[ "$#" -gt 0 ]]; do - case $1 in - --android) ANDROID="true";; - --daita) DAITA="true";; - *) - echo "Unknown parameter: $1" - exit 1 - ;; - esac - shift -done - -function unix_target_triple { - local platform - platform="$(uname -s)" - if [[ ("${platform}" == "Linux") ]]; then - local arch - arch="$(uname -m)" - echo "${arch}-unknown-linux-gnu" - elif [[ ("${platform}" == "Darwin") ]]; then - local arch - arch="$(uname -m)" - if [[ ("${arch}" == "arm64") ]]; then - arch="aarch64" - fi - echo "${arch}-apple-darwin" - else - echo "Can't deduce target dir for $platform" - return 1 - fi -} - - -function build_unix { - echo "Building wireguard-go for $1" - - # Flags for cross compiling - if [[ "$(unix_target_triple)" != "$1" ]]; then - # Linux arm - if [[ "$1" == "aarch64-unknown-linux-gnu" ]]; then - export CGO_ENABLED=1 - export GOARCH=arm64 - export CC=aarch64-linux-gnu-gcc - fi - - # Environment flags for cross compiling on macOS - if [[ "$1" == *-apple-darwin ]]; then - export CGO_ENABLED=1 - export GOOS=darwin - - local arch=x86_64 - export GOARCH=amd64 - if [[ "$1" == aarch64-* ]]; then - arch=arm64 - export GOARCH=arm64 - fi - - CC="$(xcrun -sdk "$SDKROOT" --find clang) -arch $arch -isysroot $SDKROOT" - export CC - export CFLAGS="-isysroot $SDKROOT -arch $arch -I$SDKROOT/usr/include" - export LD_LIBRARY_PATH="$SDKROOT/usr/lib" - export CGO_CFLAGS="-isysroot $SDKROOT -arch $arch" - export CGO_LDFLAGS="-isysroot $SDKROOT -arch $arch" - fi - fi - - - # Build wireguard-go as a library - mkdir -p "$BUILD_DIR/lib/$TARGET" - pushd libwg - - if [[ "$DAITA" == "true" ]]; then - go build -v --tags daita -o "$BUILD_DIR/lib/$TARGET"/libwg.a -buildmode c-archive - else - go build -v -o "$BUILD_DIR/lib/$TARGET"/libwg.a -buildmode c-archive - fi - - popd -} - -function build_android { - echo "Building wireguard-go for android" - - ./libwg/build-android.sh -} - -function build_wireguard_go { - if [[ "$ANDROID" == "true" ]]; then - build_android "$@" - return - fi - - local platform - platform="$(uname -s)"; - case "$platform" in - Linux*|Darwin*) build_unix "${TARGET:-$(unix_target_triple)}";; - *) - echo "Unsupported platform" - return 1 - ;; - esac -} - -# Ensure we are in the correct directory for the execution of this script -script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -cd "$script_dir" -build_wireguard_go "$@" diff --git a/wireguard-go-rs/build.rs b/wireguard-go-rs/build.rs index 2b8e28601dfb..08905f5e16c7 100644 --- a/wireguard-go-rs/build.rs +++ b/wireguard-go-rs/build.rs @@ -1,68 +1,194 @@ -use core::{panic, str}; -use std::{env, path::PathBuf}; +use std::{borrow::BorrowMut, env, path::PathBuf, process::Command, str}; -fn main() { - let out_dir = env::var("OUT_DIR").expect("Missing OUT_DIR"); +use anyhow::{anyhow, bail, Context}; - // Add DAITA as a conditional configuration +fn main() -> anyhow::Result<()> { + let target_os = env::var("CARGO_CFG_TARGET_OS").context("Missing 'CARGO_CFG_TARGET_OS")?; + + // Mark "daita" as a conditional configuration flag println!("cargo::rustc-check-cfg=cfg(daita)"); - let target_os = env::var("CARGO_CFG_TARGET_OS").expect("Missing 'CARGO_CFG_TARGET_OS"); - let mut cmd = std::process::Command::new("bash"); - cmd.arg("./build-wireguard-go.sh"); + // Rerun build-script if libwg (or wireguard-go) is changed + println!("cargo::rerun-if-changed=libwg"); match target_os.as_str() { - "linux" | "macos" => { - // Enable DAITA - println!(r#"cargo::rustc-cfg=daita"#); - // Tell the build script to build wireguard-go with DAITA support - cmd.arg("--daita"); - } - "android" => { - cmd.arg("--android"); - } + "linux" => build_static_lib(Os::Linux, true)?, + "macos" => build_static_lib(Os::MacOs, true)?, + "android" => build_android_dynamic_lib()?, // building wireguard-go-rs for windows is not implemented - _ => return, + _ => {} } - let output = cmd.output().expect("build-wireguard-go.sh failed"); - if !output.status.success() { - let stdout = str::from_utf8(&output.stdout).unwrap(); - let stderr = str::from_utf8(&output.stderr).unwrap(); - eprintln!("build-wireguard-go.sh failed."); - eprintln!("stdout:\n{stdout}"); - eprintln!("stderr:\n{stderr}"); - panic!(); + Ok(()) +} + +#[derive(PartialEq, Eq)] +enum Os { + MacOs, + Linux, +} + +#[derive(PartialEq, Eq)] +enum Arch { + Amd64, + Arm64, +} + +fn host_os() -> anyhow::Result { + // this ugliness is a limitation of rust, where we can't directly + // access the target triple of the build script. + if cfg!(target_os = "linux") { + Ok(Os::Linux) + } else if cfg!(target_os = "macos") { + Ok(Os::MacOs) + } else { + bail!("Unsupported host OS") } +} - if target_os.as_str() != "android" { - println!("cargo::rustc-link-lib=static=wg"); +fn host_arch() -> anyhow::Result { + if cfg!(target_arch = "x86_64") { + Ok(Arch::Amd64) + } else if cfg!(target_arch = "aarch64") { + Ok(Arch::Arm64) } else { - // NOTE: Link dynamically to libwg on Android, as go cannot produce archives - println!("cargo::rustc-link-lib=dylib=wg"); + bail!("Unsupported host architecture") } - declare_libs_dir("../build/lib"); +} - println!("cargo::rerun-if-changed=libwg"); +/// Compile libwg as a static library and place it in `OUT_DIR`. +fn build_static_lib(target_os: Os, daita: bool) -> anyhow::Result<()> { + let out_dir = env::var("OUT_DIR").context("Missing OUT_DIR")?; + let target_arch = + env::var("CARGO_CFG_TARGET_ARCH").context("Missing 'CARGO_CFG_TARGET_ARCH")?; + + let target_arch = match target_arch.as_str() { + "x86_64" => Arch::Amd64, + "aarch64" => Arch::Arm64, + _ => bail!("Unsupported architecture: {target_arch}"), + }; + + let out_file = format!("{out_dir}/libwg.a"); + let mut go_build = Command::new("go"); + go_build + .args(["build", "-v", "-o", &out_file]) + .args(["-buildmode", "c-archive"]) + .args(if daita { &["--tags", "daita"][..] } else { &[] }) + .env("CGO_ENABLED", "1") + .current_dir("./libwg"); + + // are we cross compiling? + let cross_compiling = host_os()? != target_os || host_arch()? != target_arch; + + match target_arch { + Arch::Amd64 => go_build.env("GOARCH", "amd64"), + Arch::Arm64 => go_build.env("GOARCH", "arm64"), + }; + + match target_os { + Os::Linux => { + go_build.env("GOOS", "linux"); + + if cross_compiling { + match target_arch { + Arch::Arm64 => go_build.env("CC", "aarch64-linux-gnu-gcc"), + Arch::Amd64 => bail!("cross-compiling to linux x86_64 is not implemented"), + }; + } + } + Os::MacOs => { + go_build.env("GOOS", "darwin"); - // Add `OUT_DIR` to the library search path to facilitate linking of libwg for debug artifacts, - // such as test binaries. - if cfg!(debug_assertions) { - println!("cargo::rustc-link-search={out_dir}"); + if cross_compiling { + let sdkroot = env::var("SDKROOT").context("Missing 'SDKROOT'")?; + + let c_arch = match target_arch { + Arch::Amd64 => "x86_64", + Arch::Arm64 => "arm64", + }; + + let xcrun_output = + exec(Command::new("xcrun").args(["-sdk", &sdkroot, "--find", "clang"]))?; + go_build.env("CC", xcrun_output); + + let cflags = format!("-isysroot {sdkroot} -arch {c_arch} -I{sdkroot}/usr/include"); + go_build.env("CFLAGS", cflags); + go_build.env("CGO_CFLAGS", format!("-isysroot {sdkroot} -arch {c_arch}")); + go_build.env("CGO_LDFLAGS", format!("-isysroot {sdkroot} -arch {c_arch}")); + go_build.env("LD_LIBRARY_PATH", format!("{sdkroot}/usr/lib")); + } + } } + + exec(go_build)?; + + // make sure to link to the resulting binary + println!("cargo::rustc-link-search={out_dir}"); + println!("cargo::rustc-link-lib=static=wg"); + + // if daita is enabled, also enable the corresponding rust feature flag + if daita { + println!(r#"cargo::rustc-cfg=daita"#); + } + + Ok(()) } -/// Tell linker to check `base`/$TARGET for shared libraries. -fn declare_libs_dir(base: &str) { - let target_triplet = env::var("TARGET").expect("TARGET is not set"); - let lib_dir = manifest_dir().join(base).join(target_triplet); - println!("cargo::rerun-if-changed={}", lib_dir.display()); +/// Compile libwg as a dynamic library for android and place it in `../build/lib/$TARGET`. +// NOTE: We use dynamic linking as Go cannot produce static binaries specifically for Android. +fn build_android_dynamic_lib() -> anyhow::Result<()> { + let target_triplet = env::var("TARGET").context("TARGET is not set")?; + + exec(Command::new("./libwg/build-android.sh"))?; + + // Tell linker to check `base`/$TARGET for the dynamic library. + let lib_dir = manifest_dir()?.join("../build/lib").join(target_triplet); println!("cargo::rustc-link-search={}", lib_dir.display()); + println!("cargo::rustc-link-lib=dylib=wg"); + + Ok(()) } /// Get the directory containing `Cargo.toml` -fn manifest_dir() -> PathBuf { +fn manifest_dir() -> anyhow::Result { env::var("CARGO_MANIFEST_DIR") .map(PathBuf::from) - .expect("CARGO_MANIFEST_DIR env var not set") + .context("CARGO_MANIFEST_DIR env var not set") +} + +/// Execute a command, assert that it succeeds, and return stdout as a string. +fn exec(mut command: impl BorrowMut) -> anyhow::Result { + let command = command.borrow_mut(); + + let output = command + .output() + .with_context(|| anyhow!("Failed to execute command: {command:?}"))?; + + let stdout = str::from_utf8(&output.stdout).unwrap_or("Invalid UTF-8"); + + if !output.status.success() { + let stderr = str::from_utf8(&output.stdout).unwrap_or("Invalid UTF-8"); + + eprintln!("Error from {command:?}"); + eprintln!(); + eprintln!("stdout:"); + eprintln!(); + eprintln!("{stdout}"); + eprintln!(); + eprintln!("-------"); + eprintln!("stderr:"); + eprintln!(); + eprintln!("{stderr}"); + eprintln!(); + eprintln!("-------"); + + return Err(anyhow!("Failed to execute command: {command:?}")).with_context(|| { + anyhow!( + "Command exited with a non-zero exit code: {}", + output.status + ) + }); + } + + Ok(stdout.to_string()) }