From b6174b52231fa9c4333068ad32fe7ac33f560b71 Mon Sep 17 00:00:00 2001 From: N3xed Date: Thu, 9 Sep 2021 22:12:27 +0200 Subject: [PATCH] Experimental esp-idf native cmake build (#7) * Implement native esp-idf cmake build Move `build.rs` contents to `build_pio.rs` Add feature `native` Add optional dependency `strum` * Remove main wrapper * Fix patches not getting applied, use correct patches * Clarify some `Chip` methods, fix python virtualenv path Use correct target name for `riscv32`. * Fix pio build, tidy code Fix potentially using `:` to split PATH on windows. * Add documentation to README * Change DEFAULT_SDK_DIR to `.espressif` Set min python version to 3.7. Set default build feature to pio. Make `native` feature usable. * Track `sdkconfig` and `sdkconfig_defaults` files. --- Cargo.toml | 12 +- README.md | 62 +++- build.rs | 152 +-------- build_native.rs | 418 +++++++++++++++++++++++++ build_pio.rs | 138 ++++++++ resources/cmake_project/CMakeLists.txt | 14 + resources/cmake_project/main.c | 1 + 7 files changed, 652 insertions(+), 145 deletions(-) create mode 100644 build_native.rs create mode 100644 build_pio.rs create mode 100644 resources/cmake_project/CMakeLists.txt create mode 100644 resources/cmake_project/main.c diff --git a/Cargo.toml b/Cargo.toml index 41b46ac937c0..195db4783c67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,15 +16,21 @@ links = "esp_idf" default-target = "x86_64-unknown-linux-gnu" [features] -default = ["std", "embedded-svc/std"] +default = ["std", "embedded-svc/std", "pio"] std = [] +# Use `platformio` to build the `esp-idf` +pio = [] +# Experimental: Use native `esp-idf` tooling to build it +native = ["strum"] + [dependencies] mutex-trait = "0.2" embedded-svc = "0.8.3" paste = "1" [build-dependencies] -embuild = "0.23" -anyhow = "1" +embuild = "0.23.5" +anyhow = { version = "1" } +strum = { version = "0.21", optional = true, features = ["derive"] } diff --git a/README.md b/README.md index 46a66d98452d..4310a30d05d4 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,20 @@ The ESP-IDF API in Rust, with support for each ESP chip (ESP32, ESP32S2, ESP32C3 ## Build -- The build requires the [Rust ESP32 STD compiler fork](https://github.com/ivmarkov/rust) to be configured and installed as per the instructions there. -- The relevant Espressif toolchain, as well as the ESP-IDF itself are all automatically downloaded during the build by utilizing the [cargo-pio](https://github.com/ivmarkov/cargo-pio) library crate. +- The build requires the [Rust ESP32 STD compiler fork](https://github.com/esp-rs/rust) to be configured and installed as per the instructions there. +- The relevant Espressif toolchain, as well as the `esp-idf` itself are all automatically + downloaded during the build by + - with the feature `pio` (default): utilizing [platformio](https://platformio.org/) (via + the [embuild](https://github.com/ivmarkov/embuild) crate) or + - with the feature `native` (*experimental*): utilizing native `esp-idf` tooling. - Check the ["Hello, World" demo](https://github.com/ivmarkov/rust-esp32-std-hello) for how to use and build this crate -## Bluetooth Support +## Feature `platformio` +This is currently the default for installing all build tools and building the `esp-idf` C +library. It uses [platformio](https://platformio.org/) via the +[embuild](https://github.com/ivmarkov/embuild) crate. + +### Bluetooth Support In order to enable Bluetooth support with either Bluedroid or NimBLE, there is some additional work: * Go to the root of your **binary crate** project (e.g., the ["Hello, World" demo](https://github.com/ivmarkov/rust-esp32-std-hello)) @@ -32,14 +41,55 @@ CONFIG_BTDM_CTRL_MODE_BTDM=n //CONFIG_BT_NIMBLE_ENABLED=y ``` -## Using cargo-pio to interactively modify ESP-IDF's `sdkconfig` file +### Using cargo-pio to interactively modify ESP-IDF's `sdkconfig` file To enable Bluetooth, or do other configurations to the ESP-IDF sdkconfig you might take advantage of the cargo-pio Cargo subcommand: * To install it, issue `cargo install cargo-pio --git https://github.com/ivmarkov/cargo-pio` * To open the ESP-IDF interactive menuconfig system, issue `cargo pio espidf menuconfig` in the root of your **binary crate** project * To use the generated/updated `sdkconfig` file, follow the steps described in the "Bluetooth Support" section -## More info +### More info + +If you are interested how it all works under the hood, check the [build_pio.rs](build.rs) +or script of this crate. + + +## Experimental feature `native` + +Download all tools and build the `esp-idf` using its own tooling. + +**Warning**: This is an experimental feature and subject to changes. + +Currently, this build script installs all needed tools to compile the `esp-idf` as well as the `esp-idf` itself +under `/.espressif`, this is subject to change in the future. + +### Requirements +- If using chips other than `esp32c3`: + - [Rust ESP32 compiler fork](https://github.com/esp-rs/rust) + - [libclang of the xtensa LLVM fork](https://github.com/espressif/llvm-project/releases) +- `python >= 3.7` +- `cmake >= 3.20` + +### Configuration +Environment variables are used to configure how the `esp-idf` is compiled. +The following environment variables are used by the build script: -If you are interested how it all works under the hood, check the [build.rs](https://github.com/ivmarkov/esp-idf-sys/blob/master/build.rs) script of this crate. +- `SDK_DIR`: The path to the directory where all esp-idf tools are installed, + defaults to `/.espressif`. +- `ESP_IDF_VERSION`: + The version used for the `esp-idf` can be one of the following: + - `commit:`: Uses the commit `` of the `esp-idf` repository. + Note that this will clone the whole `esp-idf` not just one commit. + - `tag:`: Uses the tag `` of the `esp-idf` repository. + - `branch:`: Uses the branch `` of the `esp-idf` repository. + - `v.` or `.`: Uses the tag `v.` of the `esp-idf` repository. + - ``: Uses the branch `` of the `esp-idf` repository. + It defaults to `v4.3`. +- `ESP_IDF_REPOSITORY`: The URL to the git repository of the `esp-idf`, defaults to . +- `ESP_IDF_SDKCONFIG_DEFAULTS`: A `;`-seperated list of paths to `sdkconfig.default` files to be used as base + values for the `sdkconfig`. +- `ESP_IDF_SDKCONFIG`: A path (absolute or relative) to the esp-idf `sdkconfig` file. +- `ESP_IDF_EXTRA_TOOLS`: A `;`-seperated list of additional tools to install with `idf_tools.py`. +- `MCU`: The mcu name (e.g. `esp32` or `esp32c3`). If not set this will be automatically detected from the + cargo target. \ No newline at end of file diff --git a/build.rs b/build.rs index 31cdc2d55aef..c4eaa860a480 100644 --- a/build.rs +++ b/build.rs @@ -1,136 +1,16 @@ -use std::convert::TryFrom; -use std::{env, path::PathBuf}; - -use anyhow::*; - -use embuild::bindgen; -use embuild::build; -use embuild::cargo; -use embuild::kconfig; -use embuild::pio; -use embuild::pio::project; - -fn main() -> Result<()> { - let pio_scons_vars = if let Some(pio_scons_vars) = project::SconsVariables::from_piofirst() { - println!("cargo:info=PIO->Cargo build detected: generating bindings only"); - - pio_scons_vars - } else { - let pio = pio::Pio::install_default()?; - - let resolution = pio::Resolver::new(pio.clone()) - .params(pio::ResolutionParams { - platform: Some("espressif32".into()), - frameworks: vec!["espidf".into()], - target: Some(env::var("TARGET")?), - ..Default::default() - }) - .resolve(true)?; - - let mut builder = - project::Builder::new(PathBuf::from(env::var("OUT_DIR")?).join("esp-idf")); - - builder - .enable_scons_dump() - .enable_c_entry_points() - .options(build::env_options_iter("ESP_IDF_SYS_PIO_CONF")?) - .files(build::tracked_globs_iter( - PathBuf::from("."), - &["patches/**"], - )?) - .files(build::tracked_env_globs_iter("ESP_IDF_SYS_GLOB")?); - - #[cfg(feature = "espidf_master")] - builder - .platform_package( - "framework-espidf", - "https://github.com/ivmarkov/esp-idf.git#master", - ) - .platform_package_patch( - PathBuf::from("patches").join("master_missing_xtensa_atomics_fix.diff"), - PathBuf::from("framework-espidf"), - ) - .platform_package_patch( - PathBuf::from("patches").join("ping_setsockopt_fix.diff"), - PathBuf::from("framework-espidf"), - ); - - #[cfg(feature = "espidf_master")] - env::set_var("IDF_MAINTAINER", "1"); - - #[cfg(not(feature = "espidf_master"))] - builder - .platform_package_patch( - PathBuf::from("patches").join("pthread_destructor_fix.diff"), - PathBuf::from("framework-espidf"), - ) - .platform_package_patch( - PathBuf::from("patches").join("ping_setsockopt_fix.diff"), - PathBuf::from("framework-espidf"), - ) - .platform_package_patch( - PathBuf::from("patches").join("missing_xtensa_atomics_fix.diff"), - PathBuf::from("framework-espidf"), - ); - - let project_path = builder.generate(&resolution)?; - - pio.build(&project_path, env::var("PROFILE")? == "release")?; - - let pio_scons_vars = project::SconsVariables::from_dump(&project_path)?; - - build::LinkArgsBuilder::try_from(&pio_scons_vars)? - .build() - .propagate(); - - pio_scons_vars - }; - - // In case other SYS crates need to have access to the ESP-IDF C headers - build::CInclArgs::try_from(&pio_scons_vars)?.propagate(); - - let cfg_args = kconfig::CfgArgs::try_from( - pio_scons_vars - .project_dir - .join(if pio_scons_vars.release_build { - "sdkconfig.release" - } else { - "sdkconfig.debug" - }) - .as_path(), - )?; - - cfg_args.propagate("ESP_IDF"); - cfg_args.output("ESP_IDF"); - - let mcu = pio_scons_vars.mcu.as_str(); - - // Output the exact ESP32 MCU, so that we and crates depending directly on us can branch using e.g. #[cfg(esp32xxx)] - println!("cargo:rustc-cfg={}", mcu); - println!("cargo:MCU={}", mcu); - - let header = PathBuf::from("src") - .join("include") - .join(if mcu == "esp8266" { - "esp-8266-rtos-sdk" - } else { - "esp-idf" - }) - .join("bindings.h"); - - cargo::track_file(&header); - - bindgen::run( - bindgen::Factory::from_scons_vars(&pio_scons_vars)? - .builder()? - .ctypes_prefix("c_types") - .header(header.to_string_lossy()) - .blacklist_function("strtold") - .blacklist_function("_strtold_r") - .clang_args(if mcu == "esp32c3" { - vec!["-target", "riscv32"] - } else { - vec![] - }), - ) -} +#[cfg(not(any(feature = "pio", feature = "native")))] +compile_error!("One of the features `pio` or `native` must be selected."); + +// Note that the feature `native` must come before `pio`. These features are really +// mutually exclusive but that would require that all dependencies specify the same +// feature so instead we prefer the `native` feature over `pio` so that if one package +// specifies it, this overrides the `pio` feature for all other dependencies too. +// See https://doc.rust-lang.org/cargo/reference/features.html#mutually-exclusive-features. +#[cfg(any(feature = "pio", feature = "native"))] +#[cfg_attr(feature = "native", path = "build_native.rs")] +#[cfg_attr(all(feature = "pio", not(feature = "native")), path = "build_pio.rs")] +mod build_impl; + +fn main() -> anyhow::Result<()> { + build_impl::main() +} \ No newline at end of file diff --git a/build_native.rs b/build_native.rs new file mode 100644 index 000000000000..9679b032f320 --- /dev/null +++ b/build_native.rs @@ -0,0 +1,418 @@ +//! Install tools and build the `esp-idf` using native tooling. + +use std::convert::TryFrom; +use std::env; +use std::ffi::OsString; +use std::path::{Path, PathBuf}; +use std::str::FromStr; + +use anyhow::*; +use embuild::cargo::IntoWarning; +use embuild::cmake::codemodel::Language; +use embuild::cmake::ObjKind; +use embuild::fs::copy_file_if_different; +use embuild::git::{CloneOptions, Repository}; +use embuild::python::{check_python_at_least, PYTHON}; +use embuild::utils::{OsStrExt, PathExt}; +use embuild::{bindgen, build, cargo, cmake, cmd, cmd_output, git, kconfig, path_buf}; +use strum::{Display, EnumString}; + +const SDK_DIR_VAR: &str = "SDK_DIR"; +const ESP_IDF_VERSION_VAR: &str = "ESP_IDF_VERSION"; +const ESP_IDF_REPOSITORY_VAR: &str = "ESP_IDF_REPOSITORY"; +const ESP_IDF_SDKCONFIG_DEFAULTS_VAR: &str = "ESP_IDF_SDKCONFIG_DEFAULTS"; +const ESP_IDF_SDKCONFIG_VAR: &str = "ESP_IDF_SDKCONFIG"; +const ESP_IDF_EXTRA_TOOLS_VAR: &str = "ESP_IDF_EXTRA_TOOLS"; +const MCU_VAR: &str = "MCU"; + +const DEFAULT_SDK_DIR: &str = ".espressif"; +const DEFAULT_ESP_IDF_REPOSITORY: &str = "https://github.com/espressif/esp-idf.git"; +const DEFAULT_ESP_IDF_VERSION: &str = "v4.3"; + +const STABLE_PATCHES: &[&str] = &[ + "patches/missing_xtensa_atomics_fix.diff", + "patches/pthread_destructor_fix.diff", + "patches/ping_setsockopt_fix.diff", +]; +const MASTER_PATCHES: &[&str] = &[ + "patches/master_missing_xtensa_atomics_fix.diff", + "patches/ping_setsockopt_fix.diff", +]; + +fn esp_idf_version() -> git::Ref { + let version = env::var(ESP_IDF_VERSION_VAR).unwrap_or(DEFAULT_ESP_IDF_VERSION.to_owned()); + let version = version.trim(); + assert!( + !version.is_empty(), + "${} (='{}') must contain a valid version", + ESP_IDF_VERSION_VAR, + version + ); + + match version.split_once(':') { + Some(("commit", c)) => git::Ref::Commit(c.to_owned()), + Some(("tag", t)) => git::Ref::Tag(t.to_owned()), + Some(("branch", b)) => git::Ref::Branch(b.to_owned()), + _ => match version.chars().next() { + Some(c) if c.is_ascii_digit() => git::Ref::Tag("v".to_owned() + version), + Some('v') if version.len() > 1 && version.chars().nth(1).unwrap().is_ascii_digit() => { + git::Ref::Tag(version.to_owned()) + } + Some(_) => git::Ref::Branch(version.to_owned()), + _ => unreachable!(), + }, + } +} + +pub fn main() -> Result<()> { + let out_dir = path_buf![env::var("OUT_DIR")?]; + let target = env::var("TARGET")?; + let workspace_dir = out_dir.pop_times(6); + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?); + + let chip = if let Some(mcu) = env::var_os(MCU_VAR) { + Chip::from_str(&mcu.to_string_lossy())? + } else { + Chip::detect(&target)? + }; + + cargo::track_env_var(SDK_DIR_VAR); + cargo::track_env_var(ESP_IDF_VERSION_VAR); + cargo::track_env_var(ESP_IDF_REPOSITORY_VAR); + cargo::track_env_var(ESP_IDF_SDKCONFIG_DEFAULTS_VAR); + cargo::track_env_var(ESP_IDF_SDKCONFIG_VAR); + cargo::track_env_var(ESP_IDF_EXTRA_TOOLS_VAR); + cargo::track_env_var(MCU_VAR); + + let sdk_dir = path_buf![env::var(SDK_DIR_VAR).unwrap_or(DEFAULT_SDK_DIR.to_owned())] + .abspath_relative_to(&workspace_dir); + + // Clone the esp-idf. + let esp_idf_dir = sdk_dir.join("esp-idf"); + let esp_idf_version = esp_idf_version(); + let esp_idf_repo = + env::var(ESP_IDF_REPOSITORY_VAR).unwrap_or(DEFAULT_ESP_IDF_REPOSITORY.to_owned()); + let mut esp_idf = Repository::new(&esp_idf_dir); + + esp_idf.clone_ext( + &esp_idf_repo, + CloneOptions::new() + .force_ref(esp_idf_version.clone()) + .depth(1), + )?; + + // Apply patches, only if the patches were not previously applied. + let patch_set = match esp_idf_version { + git::Ref::Branch(b) if esp_idf.get_default_branch()?.as_ref() == Some(&b) => { + &MASTER_PATCHES[..] + } + git::Ref::Tag(t) if t == DEFAULT_ESP_IDF_VERSION => &STABLE_PATCHES[..], + _ => { + cargo::print_warning(format_args!( + "`esp-idf` version ({:?}) not officially supported by `esp-idf-sys`. \ + Supported versions are 'master', '{}'.", + &esp_idf_version, DEFAULT_ESP_IDF_VERSION + )); + &[] + } + }; + if !patch_set.is_empty() { + esp_idf.apply_once(patch_set.iter().map(|p| manifest_dir.join(p)))?; + } + + // This is a workaround for msys or even git bash. + // When using them `idf_tools.py` prints unix paths (ex. `/c/user/` instead of + // `C:\user\`), so we correct this with an invocation of `cygpath` which converts the + // path to the windows representation. + let cygpath_works = cfg!(windows) && cmd_output!("cygpath", "--version").is_ok(); + let to_win_path = if cygpath_works { + |p: String| cmd_output!("cygpath", "-w", p).unwrap().to_string() + } else { + |p: String| p + }; + let path_var_sep = if cygpath_works || cfg!(not(windows)) { + ':' + } else { + ';' + }; + + // Create python virtualenv or use a previously installed one. + check_python_at_least(3, 7)?; + let idf_tools_py = path_buf![&esp_idf_dir, "tools", "idf_tools.py"]; + + let get_python_env_dir = || -> Result { + Ok(cmd_output!(PYTHON, &idf_tools_py, "--idf-path", &esp_idf_dir, "--quiet", "export", "--format=key-value"; + ignore_exitcode, env=("IDF_TOOLS_PATH", &sdk_dir)) + .lines() + .find(|s| s.trim_start().starts_with("IDF_PYTHON_ENV_PATH=")) + .ok_or(anyhow!("`idf_tools.py export` result contains no `IDF_PYTHON_ENV_PATH` item"))? + .trim() + .strip_prefix("IDF_PYTHON_ENV_PATH=").unwrap() + .to_string()) + }; + + let python_env_dir = get_python_env_dir().map(&to_win_path); + let python_env_dir: PathBuf = if python_env_dir.is_err() + || !Path::new(&python_env_dir.as_ref().unwrap()).exists() + { + cmd!(PYTHON, &idf_tools_py, "--idf-path", &esp_idf_dir, "--quiet", "--non-interactive", "install-python-env"; + env=("IDF_TOOLS_PATH", &sdk_dir))?; + to_win_path(get_python_env_dir()?) + } else { + python_env_dir.unwrap() + }.into(); + + // TODO: better way to get the virtualenv python executable + let python = embuild::which::which_in( + "python", + #[cfg(windows)] + Some(&python_env_dir.join("Scripts")), + #[cfg(not(windows))] + Some(&python_env_dir.join("bin")), + env::current_dir()?, + )?; + + // Install tools. + let mut tools = vec!["ninja", chip.gcc_toolchain()]; + tools.extend(chip.ulp_gcc_toolchain().iter()); + cmd!(python, &idf_tools_py, "--idf-path", &esp_idf_dir, "install"; env=("IDF_TOOLS_PATH", &sdk_dir), args=(tools))?; + + // Intall extra tools if requested, but don't fail compilation if this errors + if let Some(extra_tools) = env::var_os(ESP_IDF_EXTRA_TOOLS_VAR) { + cmd!( + python, &idf_tools_py, "--idf-path", &esp_idf_dir, "install"; + args=(extra_tools.to_string_lossy().split(';').filter(|s| !s.is_empty()).map(str::trim)), + env=("IDF_TOOLS_PATH", &sdk_dir) + ) + .into_warning(); + } + + // Get the paths to the tools. + let mut bin_paths: Vec<_> = cmd_output!(python, &idf_tools_py, "--idf-path", &esp_idf_dir, "--quiet", "export", "--format=key-value"; + ignore_exitcode, env=("IDF_TOOLS_PATH", &sdk_dir)) + .lines() + .find(|s| s.trim_start().starts_with("PATH=")) + .expect("`idf_tools.py export` result contains no `PATH` item").trim() + .strip_prefix("PATH=").unwrap() + .split(path_var_sep) + .map(|s| s.to_owned()) + .collect(); + bin_paths.pop(); + let bin_paths: Vec<_> = bin_paths + .into_iter() + .map(|s| PathBuf::from(to_win_path(s))) + .chain(env::split_paths(&env::var("PATH")?)) + .collect(); + let paths = env::join_paths(bin_paths.iter())?; + + // Create cmake project. + copy_file_if_different( + manifest_dir.join(path_buf!("resources", "cmake_project", "CMakeLists.txt")), + &out_dir, + )?; + copy_file_if_different( + manifest_dir.join(path_buf!("resources", "cmake_project", "main.c")), + &out_dir, + )?; + + // The `kconfig.cmake` script looks at this variable if it should compile `mconf` on windows. + // But this variable is also present when using git-bash which doesn't have gcc. + env::remove_var("MSYSTEM"); + + // Resolve `ESP_IDF_SDKCONFIG` and `ESP_IDF_SDKCONFIG_DEFAULTS` to an absolute path + // relative to the workspace directory if not empty. + let sdkconfig = env::var_os(ESP_IDF_SDKCONFIG_VAR) + .filter(|v| !v.is_empty()) + .map(|v| { + let path = Path::new(&v).abspath_relative_to(&workspace_dir); + cargo::track_file(&path); + path.into_os_string() + }) + .unwrap_or_else(|| OsString::new()); + + let sdkconfig_defaults = env::var_os(ESP_IDF_SDKCONFIG_DEFAULTS_VAR) + .filter(|v| !v.is_empty()) + .map(|v| -> Result { + let mut result = OsString::new(); + for s in v + .try_to_str()? + .split(';') + .filter(|v| !v.is_empty()) + .map(|v| Path::new(v).abspath_relative_to(&workspace_dir)) + { + cargo::track_file(&s); + if !result.is_empty() { + result.push(";"); + } + result.push(s); + } + + Ok(result) + }) + .unwrap_or_else(|| Ok(OsString::new()))?; + + let cmake_toolchain_file = + path_buf![&esp_idf_dir, "tools", "cmake", chip.cmake_toolchain_file()]; + + // Get the asm, C and C++ flags from the toolchain file, these would otherwise get + // overwritten because `cmake::Config` also sets these (see + // https://github.com/espressif/esp-idf/issues/7507). + let (asm_flags, c_flags, cxx_flags) = { + let mut vars = cmake::get_script_variables(&cmake_toolchain_file)?; + ( + vars.remove("CMAKE_ASM_FLAGS").unwrap_or_default(), + vars.remove("CMAKE_C_FLAGS").unwrap_or_default(), + vars.remove("CMAKE_CXX_FLAGS").unwrap_or_default(), + ) + }; + + // `cmake::Config` automatically uses `/build` and there is no way to query + // what build directory it sets, so we hard-code it. + let cmake_build_dir = out_dir.join("build"); + + let query = cmake::Query::new( + &cmake_build_dir, + "cargo", + &[ObjKind::Codemodel, ObjKind::Toolchains], + )?; + + // Build the esp-idf. + cmake::Config::new(&out_dir) + .generator("Ninja") + .out_dir(&out_dir) + .no_build_target(true) + .define("CMAKE_TOOLCHAIN_FILE", &cmake_toolchain_file) + .always_configure(true) + .pic(false) + .asmflag(asm_flags) + .cflag(c_flags) + .cxxflag(cxx_flags) + .env("IDF_PATH", &esp_idf_dir) + .env("PATH", &paths) + .env("SDKCONFIG", sdkconfig) + .env("SDKCONFIG_DEFAULTS", sdkconfig_defaults) + .env("IDF_TARGET", &chip.to_string()) + .build(); + + let replies = query.get_replies()?; + let target = replies + .get_codemodel()? + .into_first_conf() + .get_target("libespidf.elf") + .unwrap_or_else(|| { + bail!("Could not read build information from cmake: Target 'libespidf.elf' not found",) + })?; + + let compiler = replies + .get_toolchains() + .and_then(|mut t| { + t.take(Language::C) + .ok_or_else(|| Error::msg("No C toolchain")) + }) + .and_then(|t| { + t.compiler + .path + .ok_or_else(|| Error::msg("No compiler path set")) + }) + .context("Could not determine the compiler from cmake")?; + + let header_file = path_buf![&manifest_dir, "src", "include", "esp-idf", "bindings.h"]; + + bindgen::run( + bindgen::Factory::from_cmake(&target.compile_groups[0])? + .with_linker(&compiler) + .builder()? + .ctypes_prefix("c_types") + .header(header_file.try_to_str()?) + .blacklist_function("strtold") + .blacklist_function("_strtold_r") + .clang_args(["-target", chip.clang_target()]), + )?; + + // Output the exact ESP32 MCU, so that we and crates depending directly on us can branch using e.g. #[cfg(esp32xxx)] + cargo::set_rustc_cfg(chip, ""); + cargo::set_metadata("MCU", chip); + + build::LinkArgsBuilder::try_from(&target.link.unwrap())? + .linker(&compiler) + .working_directory(&cmake_build_dir) + .force_ldproxy(true) + .build()? + .propagate(); + + // In case other SYS crates need to have access to the ESP-IDF C headers + build::CInclArgs::try_from(&target.compile_groups[0])?.propagate(); + + let sdkconfig_json = path_buf![&cmake_build_dir, "config", "sdkconfig.json"]; + let cfgs = kconfig::CfgArgs::try_from_json(&sdkconfig_json) + .with_context(|| anyhow!("Failed to read '{:?}'", sdkconfig_json))?; + cfgs.propagate("ESP_IDF"); + cfgs.output("ESP_IDF"); + + Ok(()) +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, EnumString)] +#[repr(u32)] +pub enum Chip { + /// Xtensa LX7 based dual core + #[strum(serialize = "esp32")] + ESP32 = 0, + /// Xtensa LX7 based single core + #[strum(serialize = "esp32s2")] + ESP32S2, + /// Xtensa LX7 based single core + #[strum(serialize = "esp32s3")] + ESP32S3, + /// RISC-V based single core + #[strum(serialize = "esp32c3")] + ESP32C3, +} + +impl Chip { + pub fn detect(rust_target_triple: &str) -> Result { + if rust_target_triple.starts_with("xtensa-esp") { + if rust_target_triple.contains("esp32s3") { + return Ok(Chip::ESP32S3); + } else if rust_target_triple.contains("esp32s2") { + return Ok(Chip::ESP32S2); + } else { + return Ok(Chip::ESP32); + } + } else if rust_target_triple.starts_with("riscv32imc-esp") { + return Ok(Chip::ESP32C3); + } + bail!("Unsupported target '{}'", rust_target_triple) + } + + /// The name of the gcc toolchain (to compile the `esp-idf`) for `idf_tools.py`. + pub fn gcc_toolchain(self) -> &'static str { + match self { + Self::ESP32 => "xtensa-esp32-elf", + Self::ESP32S2 => "xtensa-esp32s2-elf", + Self::ESP32S3 => "xtensa-esp32s3-elf", + Self::ESP32C3 => "riscv32-esp-elf", + } + } + + /// The name of the gcc toolchain for the ultra low-power co-processor for + /// `idf_tools.py`. + pub fn ulp_gcc_toolchain(self) -> Option<&'static str> { + match self { + Self::ESP32 => Some("esp32ulp-elf"), + Self::ESP32S2 => Some("esp32s2ulp-elf"), + _ => None, + } + } + + pub fn cmake_toolchain_file(self) -> String { + format!("toolchain-{}.cmake", self) + } + + pub fn clang_target(self) -> &'static str { + match self { + Self::ESP32 | Self::ESP32S2 | Self::ESP32S3 => "xtensa", + Self::ESP32C3 => "riscv32", + } + } +} diff --git a/build_pio.rs b/build_pio.rs new file mode 100644 index 000000000000..03cd573ca1cf --- /dev/null +++ b/build_pio.rs @@ -0,0 +1,138 @@ +//! Install tools and build the `esp-idf` using `platformio`. + +use std::convert::TryFrom; +use std::{env, path::PathBuf}; + +use anyhow::*; + +use embuild::bindgen; +use embuild::build; +use embuild::cargo; +use embuild::kconfig; +use embuild::pio; +use embuild::pio::project; + +pub fn main() -> Result<()> { + let pio_scons_vars = if let Some(pio_scons_vars) = project::SconsVariables::from_piofirst() { + println!("cargo:info=PIO->Cargo build detected: generating bindings only"); + + pio_scons_vars + } else { + let pio = pio::Pio::install_default()?; + + let resolution = pio::Resolver::new(pio.clone()) + .params(pio::ResolutionParams { + platform: Some("espressif32".into()), + frameworks: vec!["espidf".into()], + target: Some(env::var("TARGET")?), + ..Default::default() + }) + .resolve(true)?; + + let mut builder = + project::Builder::new(PathBuf::from(env::var("OUT_DIR")?).join("esp-idf")); + + builder + .enable_scons_dump() + .enable_c_entry_points() + .options(build::env_options_iter("ESP_IDF_SYS_PIO_CONF")?) + .files(build::tracked_globs_iter( + PathBuf::from("."), + &["patches/**"], + )?) + .files(build::tracked_env_globs_iter("ESP_IDF_SYS_GLOB")?); + + #[cfg(feature = "espidf_master")] + builder + .platform_package( + "framework-espidf", + "https://github.com/ivmarkov/esp-idf.git#master", + ) + .platform_package_patch( + PathBuf::from("patches").join("master_missing_xtensa_atomics_fix.diff"), + PathBuf::from("framework-espidf"), + ) + .platform_package_patch( + PathBuf::from("patches").join("ping_setsockopt_fix.diff"), + PathBuf::from("framework-espidf"), + ); + + #[cfg(feature = "espidf_master")] + env::set_var("IDF_MAINTAINER", "1"); + + #[cfg(not(feature = "espidf_master"))] + builder + .platform_package_patch( + PathBuf::from("patches").join("pthread_destructor_fix.diff"), + PathBuf::from("framework-espidf"), + ) + .platform_package_patch( + PathBuf::from("patches").join("ping_setsockopt_fix.diff"), + PathBuf::from("framework-espidf"), + ) + .platform_package_patch( + PathBuf::from("patches").join("missing_xtensa_atomics_fix.diff"), + PathBuf::from("framework-espidf"), + ); + + let project_path = builder.generate(&resolution)?; + + pio.build(&project_path, env::var("PROFILE")? == "release")?; + + let pio_scons_vars = project::SconsVariables::from_dump(&project_path)?; + + build::LinkArgsBuilder::try_from(&pio_scons_vars)? + .build()? + .propagate(); + + pio_scons_vars + }; + + // In case other SYS crates need to have access to the ESP-IDF C headers + build::CInclArgs::try_from(&pio_scons_vars)?.propagate(); + + let cfg_args = kconfig::CfgArgs::try_from( + pio_scons_vars + .project_dir + .join(if pio_scons_vars.release_build { + "sdkconfig.release" + } else { + "sdkconfig.debug" + }) + .as_path(), + )?; + + cfg_args.propagate("ESP_IDF"); + cfg_args.output("ESP_IDF"); + + let mcu = pio_scons_vars.mcu.as_str(); + + // Output the exact ESP32 MCU, so that we and crates depending directly on us can branch using e.g. #[cfg(esp32xxx)] + cargo::set_rustc_cfg(mcu, ""); + cargo::set_metadata("MCU", mcu); + + let header = PathBuf::from("src") + .join("include") + .join(if mcu == "esp8266" { + "esp-8266-rtos-sdk" + } else { + "esp-idf" + }) + .join("bindings.h"); + + cargo::track_file(&header); + + bindgen::run( + bindgen::Factory::from_scons_vars(&pio_scons_vars)? + .builder()? + .ctypes_prefix("c_types") + .header(header.to_string_lossy()) + .blacklist_function("strtold") + .blacklist_function("_strtold_r") + .clang_args(if mcu == "esp32c3" { + vec!["-target", "riscv32"] + } else { + vec![] + }), + ) +} diff --git a/resources/cmake_project/CMakeLists.txt b/resources/cmake_project/CMakeLists.txt new file mode 100644 index 000000000000..788c52301009 --- /dev/null +++ b/resources/cmake_project/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.20) + +set(SDKCONFIG $ENV{SDKCONFIG}) +set(SDKCONFIG_DEFAULTS $ENV{SDKCONFIG_DEFAULTS}) + +include($ENV{IDF_PATH}/tools/cmake/idf.cmake) +project(libespidf C) + +idf_build_process($ENV{IDF_TARGET}) +idf_build_get_property(aliases BUILD_COMPONENT_ALIASES) + +add_executable(libespidf.elf main.c) +target_link_libraries(libespidf.elf PUBLIC "${aliases}") +idf_build_executable(libespidf.elf) \ No newline at end of file diff --git a/resources/cmake_project/main.c b/resources/cmake_project/main.c new file mode 100644 index 000000000000..3b5b685b9948 --- /dev/null +++ b/resources/cmake_project/main.c @@ -0,0 +1 @@ +void app_main() {} \ No newline at end of file