├── template ├── src │ ├── lib.rs │ └── bin │ │ ├── main.rs │ │ └── async_main.rs ├── .clippy.toml ├── .vscode │ ├── tasks.json │ ├── extensions.json │ ├── settings.json │ └── launch.json ├── wokwi.toml ├── rust-toolchain.toml ├── .helix │ └── languages.toml ├── .gitignore ├── .nvim.lua ├── diagram.json ├── tests │ ├── hello_test.rs │ └── async_hello_test.rs ├── .zed │ └── settings.json ├── .cargo │ └── config.toml ├── .github │ └── workflows │ │ └── rust_ci.yml ├── build.rs ├── template.yaml └── Cargo.toml ├── rust-toolchain.toml ├── .cargo └── config.toml ├── .gitignore ├── .github └── workflows │ ├── issue_handler.yml │ ├── changelog.yml │ ├── cd.yml │ └── ci.yml ├── xtask ├── Cargo.toml └── src │ └── main.rs ├── LICENSE-MIT ├── Cargo.toml ├── src ├── lib.rs ├── template.rs ├── cargo.rs ├── toolchain.rs ├── check.rs ├── tui.rs ├── config.rs └── main.rs ├── README.md ├── CHANGELOG.md └── LICENSE-APACHE /template/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] -------------------------------------------------------------------------------- /template/.clippy.toml: -------------------------------------------------------------------------------- 1 | stack-size-threshold = 1024 2 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | xtask = "run --manifest-path xtask/Cargo.toml --" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | src/template_files.rs 3 | template/Cargo.lock 4 | /.vscode 5 | /.zed 6 | -------------------------------------------------------------------------------- /template/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | //INCLUDEFILE option("vscode") 2 | { 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "label": "build-debug", 7 | "command": "cargo build", 8 | "type": "shell", 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /template/wokwi.toml: -------------------------------------------------------------------------------- 1 | #INCLUDEFILE option("wokwi") 2 | [wokwi] 3 | version = 1 4 | gdbServerPort = 3333 5 | #REPLACE project-name project-name && rust_target rust_target 6 | elf = "target/rust_target/debug/project-name" 7 | #REPLACE project-name project-name && rust_target rust_target 8 | firmware = "target/rust_target/debug/project-name" 9 | -------------------------------------------------------------------------------- /template/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | //INCLUDEFILE option("vscode") 2 | { 3 | "recommendations": [ 4 | "rust-lang.rust-analyzer", 5 | "tamasfe.even-better-toml", 6 | //IF option("wokwi") 7 | "Wokwi.wokwi-vscode", 8 | //ENDIF 9 | //IF option("probe-rs") 10 | "probe-rs.probe-rs-debugger", 11 | //ENDIF 12 | "fill-labs.dependi" 13 | ] 14 | } -------------------------------------------------------------------------------- /.github/workflows/issue_handler.yml: -------------------------------------------------------------------------------- 1 | name: Add new issues to project 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | 8 | jobs: 9 | add-to-project: 10 | name: Add issue to project 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/add-to-project@v0.5.0 14 | with: 15 | project-url: https://github.com/orgs/esp-rs/projects/2 16 | github-token: ${{ secrets.PAT }} -------------------------------------------------------------------------------- /template/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | #REPLACE stable rust_toolchain && esp rust_toolchain 3 | #IF option("riscv") 4 | #IF option("stack-smashing-protection") 5 | #+channel = "nightly" 6 | #ELSE 7 | channel = "stable" 8 | #ENDIF 9 | components = ["rust-src"] 10 | #REPLACE riscv32imac-unknown-none-elf rust_target 11 | targets = ["riscv32imac-unknown-none-elf"] 12 | #ELIF option("xtensa") 13 | #+channel = "esp" 14 | #ENDIF 15 | -------------------------------------------------------------------------------- /xtask/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xtask" 3 | version = "0.0.0" 4 | edition = "2024" 5 | publish = false 6 | 7 | [dependencies] 8 | anyhow = "1.0.100" 9 | clap = { version = "4.5.49", features = ["derive"] } 10 | env_logger = "0.11.8" 11 | esp-metadata = { version = "0.9.0", features = ["clap"] } 12 | esp-generate = { path = "..", default-features = false } 13 | log = "0.4.28" 14 | tempfile = "3.23.0" 15 | serde_yaml = "0.9.33" 16 | -------------------------------------------------------------------------------- /template/.helix/languages.toml: -------------------------------------------------------------------------------- 1 | #INCLUDEFILE option("helix") 2 | [[language]] 3 | name = "rust" 4 | 5 | #IF option("xtensa") 6 | [language-server.rust-analyzer] 7 | environment.RUSTUP_TOOLCHAIN = "stable" 8 | 9 | #ENDIF 10 | [language-server.rust-analyzer.config] 11 | cargo.allTargets = false 12 | #REPLACE riscv32imac-unknown-none-elf rust_target 13 | cargo.target = "riscv32imac-unknown-none-elf" 14 | #IF option("xtensa") 15 | check.extraEnv.RUSTUP_TOOLCHAIN = "esp" 16 | cargo.extraEnv.RUSTUP_TOOLCHAIN = "esp" 17 | #ENDIF 18 | -------------------------------------------------------------------------------- /.github/workflows/changelog.yml: -------------------------------------------------------------------------------- 1 | name: Changelog check 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened, labeled, unlabeled, synchronize] 6 | 7 | jobs: 8 | changelog: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Check that changelog updated 15 | uses: dangoslen/changelog-enforcer@v3 16 | with: 17 | skipLabels: "skip-changelog" 18 | missingUpdateErrorMessage: "Please add a changelog entry in the CHANGELOG.md file." 19 | -------------------------------------------------------------------------------- /template/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | //INCLUDEFILE option("vscode") 2 | { 3 | "rust-analyzer.cargo.allTargets": false, 4 | //REPLACE riscv32imac-unknown-none-elf rust_target 5 | "rust-analyzer.cargo.target": "riscv32imac-unknown-none-elf", 6 | //IF option("xtensa") 7 | "rust-analyzer.server.extraEnv": { 8 | "RUSTUP_TOOLCHAIN": "stable" 9 | }, 10 | "rust-analyzer.check.extraEnv": { 11 | "RUSTUP_TOOLCHAIN": "esp" 12 | }, 13 | "rust-analyzer.cargo.extraEnv": { 14 | "RUSTUP_TOOLCHAIN": "esp" 15 | }, 16 | //ENDIF 17 | } -------------------------------------------------------------------------------- /template/.gitignore: -------------------------------------------------------------------------------- 1 | # will have compiled files and executables 2 | debug/ 3 | target/ 4 | 5 | # Editor configuration 6 | .vscode/ 7 | .zed/ 8 | .helix/ 9 | .nvim.lua 10 | 11 | # These are backup files generated by rustfmt 12 | **/*.rs.bk 13 | 14 | # MSVC Windows builds of rustc generate these, which store debugging information 15 | *.pdb 16 | 17 | # RustRover 18 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 19 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 20 | # and can be added to the global gitignore or merged into this file. For a more nuclear 21 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 22 | #.idea/ 23 | -------------------------------------------------------------------------------- /template/.nvim.lua: -------------------------------------------------------------------------------- 1 | --INCLUDEFILE option("neovim") 2 | -- You must enable the exrc setting in neovim for this config file to be used. 3 | local rust_analyzer = { 4 | cargo = { 5 | --REPLACE riscv32imac-unknown-none-elf rust_target 6 | target = "riscv32imac-unknown-none-elf", 7 | allTargets = false, 8 | }, 9 | } 10 | --IF option("xtensa") 11 | rust_analyzer.cargo.extraEnv = { RUST_TOOLCHAIN = "esp" } 12 | rust_analyzer.check = { extraEnv = { RUST_TOOLCHAIN = "esp" } } 13 | rust_analyzer.server = { extraEnv = { RUST_TOOLCHAIN = "stable" } } 14 | --ENDIF 15 | 16 | -- Note the neovim name of the language server is rust_analyzer with an underscore. 17 | vim.lsp.config("rust_analyzer", { 18 | settings = { 19 | ["rust-analyzer"] = rust_analyzer 20 | }, 21 | }) 22 | -------------------------------------------------------------------------------- /template/diagram.json: -------------------------------------------------------------------------------- 1 | //INCLUDEFILE option("wokwi") 2 | { 3 | "version": 1, 4 | "editor": "wokwi", 5 | "parts": [ 6 | { 7 | //REPLACE wokwi-board wokwi-board 8 | "type": "wokwi-board", 9 | "id": "esp", 10 | "top": 0.59, 11 | "left": 0.67, 12 | "attrs": { 13 | "flashSize": "16" 14 | } 15 | } 16 | ], 17 | "connections": [ 18 | [ 19 | "esp:TX", 20 | "$serialMonitor:RX", 21 | "", 22 | [] 23 | ], 24 | [ 25 | "esp:RX", 26 | "$serialMonitor:TX", 27 | "", 28 | [] 29 | ] 30 | ], 31 | "serialMonitor": { 32 | "display": "terminal", 33 | "convertEol": true 34 | } 35 | } -------------------------------------------------------------------------------- /template/tests/hello_test.rs: -------------------------------------------------------------------------------- 1 | //INCLUDEFILE !option("embassy") && option("embedded-test") 2 | //! Demo test suite using embedded-test 3 | //! 4 | //! You can run this using `cargo test` as usual. 5 | 6 | #![no_std] 7 | #![no_main] 8 | 9 | esp_bootloader_esp_idf::esp_app_desc!(); 10 | 11 | #[cfg(test)] 12 | #[embedded_test::tests] 13 | mod tests { 14 | //IF option("defmt") 15 | use defmt::assert_eq; 16 | //ENDIF 17 | use esp_hal as _; 18 | 19 | #[init] 20 | fn init() { 21 | let _ = esp_hal::init(esp_hal::Config::default()); 22 | 23 | //IF option("defmt") 24 | rtt_target::rtt_init_defmt!(); 25 | //ENDIF 26 | } 27 | 28 | #[test] 29 | fn hello_test() { 30 | //IF option("defmt") 31 | defmt::info!("Running test!"); 32 | //ENDIF 33 | 34 | assert_eq!(1 + 1, 2); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /template/.zed/settings.json: -------------------------------------------------------------------------------- 1 | //INCLUDEFILE option("zed") 2 | { 3 | "lsp": { 4 | "rust-analyzer": { 5 | "initialization_options": { 6 | "cargo": { 7 | "allTargets": false, 8 | //REPLACE riscv32imac-unknown-none-elf rust_target 9 | "target": "riscv32imac-unknown-none-elf", 10 | //IF option("xtensa") 11 | "extraEnv": { 12 | "RUSTUP_TOOLCHAIN": "esp" 13 | } 14 | //ENDIF 15 | }, 16 | //IF option("xtensa") 17 | "server": { 18 | "extraEnv": { 19 | "RUSTUP_TOOLCHAIN": "stable" 20 | } 21 | }, 22 | "check": { 23 | "extraEnv": { 24 | "RUSTUP_TOOLCHAIN": "esp" 25 | } 26 | }, 27 | //ENDIF 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 esp-rs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /template/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | #REPLACE riscv32imac-unknown-none-elf rust_target 2 | [target.riscv32imac-unknown-none-elf] 3 | #IF option("probe-rs") 4 | #REPLACE esp32c6 mcu 5 | runner = "probe-rs run --chip=esp32c6 --preverify --always-print-stacktrace --no-location --catch-hardfault" 6 | #ELIF option("defmt") 7 | #REPLACE esp32c6 mcu 8 | #+runner = "espflash flash --monitor --chip esp32c6 --log-format defmt" 9 | #ELSE 10 | #REPLACE esp32c6 mcu 11 | #+runner = "espflash flash --monitor --chip esp32c6" 12 | #ENDIF 13 | 14 | [env] 15 | #IF option("defmt") 16 | DEFMT_LOG="info" 17 | #ELIF option("log") 18 | ESP_LOG="info" 19 | #ENDIF 20 | 21 | [build] 22 | rustflags = [ 23 | #IF option("xtensa") 24 | "-C", "link-arg=-nostartfiles", 25 | #ELIF option("riscv") 26 | # Required to obtain backtraces (e.g. when using the "esp-backtrace" crate.) 27 | # NOTE: May negatively impact performance of produced code 28 | "-C", "force-frame-pointers", 29 | #ENDIF 30 | #IF option("stack-smashing-protection") 31 | "-Z", "stack-protector=all", 32 | #ENDIF 33 | ] 34 | 35 | #REPLACE riscv32imac-unknown-none-elf rust_target 36 | target = "riscv32imac-unknown-none-elf" 37 | 38 | [unstable] 39 | #IF option("alloc") 40 | build-std = ["alloc", "core"] 41 | #ELSE 42 | #+build-std = ["core"] 43 | #ENDIF 44 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "esp-generate" 3 | version = "1.1.0" 4 | edition = "2024" 5 | rust-version = "1.86" 6 | description = "Template generation tool to create no_std applications targeting Espressif's chips" 7 | repository = "https://github.com/esp-rs/esp-generate" 8 | license = "MIT OR Apache-2.0" 9 | keywords = ["esp32", "template"] 10 | categories = ["command-line-utilities", "template-engine"] 11 | 12 | include = [ 13 | "Cargo.toml", 14 | "LICENSE-APACHE", 15 | "LICENSE-MIT", 16 | "README.md", 17 | "src", 18 | "template", 19 | ] 20 | 21 | [features] 22 | default = ["update-informer"] 23 | update-informer = ["dep:update-informer"] 24 | 25 | [dependencies] 26 | anyhow = "1.0.100" 27 | strum = "0.27.2" 28 | inquire = "0.9.1" 29 | clap = { version = "4.5.53", features = ["derive"] } 30 | env_logger = "0.11.8" 31 | esp-metadata = { version = "0.9.0", features = ["clap"] } 32 | log = "0.4.29" 33 | ratatui = { version = "0.29.0", features = ["crossterm", "unstable"] } 34 | somni-expr = "0.2.0" 35 | taplo = "0.14.0" 36 | update-informer = { version = "1.3.0", optional = true } 37 | serde = { version = "1.0.228", features = ["derive"] } 38 | serde_yaml = "0.9.33" 39 | toml_edit = "0.23.9" 40 | 41 | [build-dependencies] 42 | quote = "1.0.42" 43 | walkdir = "2.5.0" 44 | -------------------------------------------------------------------------------- /template/tests/async_hello_test.rs: -------------------------------------------------------------------------------- 1 | //INCLUDEFILE option("embassy") && option("embedded-test") 2 | //INCLUDE_AS tests/hello_test.rs 3 | //! Demo test suite using embedded-test 4 | //! 5 | //! You can run this using `cargo test` as usual. 6 | 7 | #![no_std] 8 | #![no_main] 9 | 10 | esp_bootloader_esp_idf::esp_app_desc!(); 11 | 12 | #[cfg(test)] 13 | #[embedded_test::tests(executor = esp_rtos::embassy::Executor::new())] 14 | mod tests { 15 | //IF option("defmt") 16 | use defmt::assert_eq; 17 | //ENDIF 18 | 19 | #[init] 20 | fn init() { 21 | let peripherals = esp_hal::init(esp_hal::Config::default()); 22 | 23 | let timg1 = esp_hal::timer::timg::TimerGroup::new(peripherals.TIMG1); 24 | //IF option("esp32") || option("esp32s2") || option("esp32s3") 25 | esp_rtos::start(timg1.timer0); 26 | //ELSE 27 | let sw_interrupt = 28 | esp_hal::interrupt::software::SoftwareInterruptControl::new(peripherals.SW_INTERRUPT); 29 | esp_rtos::start(timg1.timer0, sw_interrupt.software_interrupt0); 30 | //ENDIF 31 | 32 | //IF option("defmt") 33 | rtt_target::rtt_init_defmt!(); 34 | //ENDIF 35 | } 36 | 37 | #[test] 38 | async fn hello_test() { 39 | //IF option("defmt") 40 | defmt::info!("Running test!"); 41 | //ENDIF 42 | 43 | embassy_time::Timer::after(embassy_time::Duration::from_millis(100)).await; 44 | assert_eq!(1 + 1, 2); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /template/.github/workflows/rust_ci.yml: -------------------------------------------------------------------------------- 1 | #INCLUDEFILE option("ci") 2 | name: Continuous Integration 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | paths-ignore: 9 | - "**/README.md" 10 | pull_request: 11 | workflow_dispatch: 12 | 13 | env: 14 | CARGO_TERM_COLOR: always 15 | #IF option("xtensa") 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | #ENDIF 18 | 19 | jobs: 20 | rust-checks: 21 | name: Rust Checks 22 | runs-on: ubuntu-latest 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | action: 27 | - command: build 28 | args: --release 29 | - command: fmt 30 | args: --all -- --check 31 | - command: clippy 32 | args: --all-features --workspace -- -D warnings 33 | steps: 34 | - name: Checkout repository 35 | uses: actions/checkout@v4 36 | - name: Setup Rust 37 | #IF option("riscv") 38 | uses: dtolnay/rust-toolchain@v1 39 | with: 40 | #REPLACE riscv32imac-unknown-none-elf rust_target 41 | target: riscv32imac-unknown-none-elf 42 | toolchain: stable 43 | components: rust-src, rustfmt, clippy 44 | #ELIF option("xtensa") 45 | #+ uses: esp-rs/xtensa-toolchain@v1.5 46 | #+ with: 47 | #+ default: true 48 | #REPLACE esp32 mcu 49 | #+ buildtargets: esp32 50 | #+ ldproxy: false 51 | #ENDIF 52 | - name: Enable caching 53 | uses: Swatinem/rust-cache@v2 54 | - name: Run command 55 | run: cargo ${{ matrix.action.command }} ${{ matrix.action.args }} 56 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod cargo; 2 | pub mod config; 3 | pub mod template; 4 | 5 | /// This turns a list of strings into a sentence, and appends it to the base string. 6 | /// 7 | /// # Example 8 | /// 9 | /// ```rust 10 | /// # use esp_generate::append_list_as_sentence; 11 | /// let list = &["foo", "bar", "baz"]; 12 | /// let sentence = append_list_as_sentence("Here is a sentence.", "My elements are", list); 13 | /// assert_eq!(sentence, "Here is a sentence. My elements are `foo`, `bar` and `baz`."); 14 | /// 15 | /// let list = &["foo", "bar", "baz"]; 16 | /// let sentence = append_list_as_sentence("The following list is problematic:", "", list); 17 | /// assert_eq!(sentence, "The following list is problematic: `foo`, `bar` and `baz`."); 18 | /// ``` 19 | pub fn append_list_as_sentence>(base: &str, word: &str, els: &[S]) -> String { 20 | if !els.is_empty() { 21 | let mut requires = String::new(); 22 | 23 | if !base.is_empty() { 24 | requires.push_str(base); 25 | requires.push(' '); 26 | } 27 | 28 | for (i, r) in els.iter().enumerate() { 29 | if i == 0 { 30 | if !word.is_empty() { 31 | requires.push_str(word); 32 | requires.push(' '); 33 | } 34 | } else if i == els.len() - 1 { 35 | requires.push_str(" and "); 36 | } else { 37 | requires.push_str(", "); 38 | }; 39 | 40 | requires.push('`'); 41 | requires.push_str(r.as_ref()); 42 | requires.push('`'); 43 | } 44 | requires.push('.'); 45 | 46 | requires 47 | } else { 48 | base.to_string() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /template/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | //INCLUDEFILE option("vscode") 2 | { 3 | "version": "0.2.0", 4 | "configurations": [ 5 | //IF option("probe-rs") 6 | { 7 | "type": "probe-rs-debug", 8 | "request": "launch", 9 | "name": "Launch", 10 | "cwd": "${workspaceFolder}", 11 | "preLaunchTask": "build-debug", 12 | //REPLACE esp32c3 mcu 13 | "chip": "esp32c3", 14 | "flashingConfig": { 15 | "flashingEnabled": true, 16 | "haltAfterReset": true, 17 | "formatOptions": { 18 | "binaryFormat": "idf" 19 | } 20 | }, 21 | "coreConfigs": [ 22 | { 23 | "coreIndex": 0, 24 | //REPLACE riscv32imc-unknown-none-elf rust_target 25 | "programBinary": "target/riscv32imc-unknown-none-elf/debug/${workspaceFolderBasename}", 26 | "rttEnabled": true, 27 | //IF option("defmt") 28 | "rttChannelFormats": [ 29 | { 30 | "channelNumber": 0, 31 | "dataFormat": "Defmt", 32 | } 33 | ], 34 | //ENDIF 35 | } 36 | ] 37 | }, 38 | { 39 | "type": "probe-rs-debug", 40 | "request": "attach", 41 | "name": "Attach", 42 | "cwd": "${workspaceFolder}", 43 | //REPLACE esp32c3 mcu 44 | "chip": "esp32c3", 45 | "coreConfigs": [ 46 | { 47 | "coreIndex": 0, 48 | //REPLACE riscv32imc-unknown-none-elf rust_target 49 | "programBinary": "target/riscv32imc-unknown-none-elf/debug/${workspaceFolderBasename}", 50 | "rttEnabled": true, 51 | //IF option("defmt") 52 | "rttChannelFormats": [ 53 | { 54 | "channelNumber": 0, 55 | "dataFormat": "Defmt", 56 | } 57 | ], 58 | //ENDIF 59 | } 60 | ] 61 | } 62 | //ENDIF 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /template/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | linker_be_nice(); 3 | //IF option("embedded-test") 4 | println!("cargo:rustc-link-arg-tests=-Tembedded-test.x"); 5 | //ENDIF 6 | //IF option("defmt") 7 | println!("cargo:rustc-link-arg=-Tdefmt.x"); 8 | //ENDIF 9 | // make sure linkall.x is the last linker script (otherwise might cause problems with flip-link) 10 | println!("cargo:rustc-link-arg=-Tlinkall.x"); 11 | } 12 | 13 | fn linker_be_nice() { 14 | let args: Vec = std::env::args().collect(); 15 | if args.len() > 1 { 16 | let kind = &args[1]; 17 | let what = &args[2]; 18 | 19 | match kind.as_str() { 20 | "undefined-symbol" => match what.as_str() { 21 | what if what.starts_with("_defmt_") => { 22 | eprintln!(); 23 | eprintln!( 24 | "💡 `defmt` not found - make sure `defmt.x` is added as a linker script and you have included `use defmt_rtt as _;`" 25 | ); 26 | eprintln!(); 27 | } 28 | "_stack_start" => { 29 | eprintln!(); 30 | eprintln!("💡 Is the linker script `linkall.x` missing?"); 31 | eprintln!(); 32 | } 33 | what if what.starts_with("esp_rtos_") => { 34 | eprintln!(); 35 | eprintln!( 36 | "💡 `esp-radio` has no scheduler enabled. Make sure you have initialized `esp-rtos` or provided an external scheduler." 37 | ); 38 | eprintln!(); 39 | } 40 | "embedded_test_linker_file_not_added_to_rustflags" => { 41 | eprintln!(); 42 | eprintln!( 43 | "💡 `embedded-test` not found - make sure `embedded-test.x` is added as a linker script for tests" 44 | ); 45 | eprintln!(); 46 | } 47 | "free" 48 | | "malloc" 49 | | "calloc" 50 | | "get_free_internal_heap_size" 51 | | "malloc_internal" 52 | | "realloc_internal" 53 | | "calloc_internal" 54 | | "free_internal" => { 55 | eprintln!(); 56 | eprintln!( 57 | "💡 Did you forget the `esp-alloc` dependency or didn't enable the `compat` feature on it?" 58 | ); 59 | eprintln!(); 60 | } 61 | _ => (), 62 | }, 63 | // we don't have anything helpful for "missing-lib" yet 64 | _ => { 65 | std::process::exit(1); 66 | } 67 | } 68 | 69 | std::process::exit(0); 70 | } 71 | 72 | //IF option("xtensa") 73 | println!( 74 | "cargo:rustc-link-arg=-Wl,--error-handling-script={}", 75 | std::env::current_exe().unwrap().display() 76 | ); 77 | //ELIF option("riscv") 78 | println!( 79 | "cargo:rustc-link-arg=--error-handling-script={}", 80 | std::env::current_exe().unwrap().display() 81 | ); 82 | //ENDIF 83 | } 84 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Deployment 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 10 | 11 | jobs: 12 | publish-artifacts: 13 | name: Generating artifacts for ${{ matrix.job.target }} 14 | runs-on: ${{ matrix.job.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | job: 19 | - os: macos-latest 20 | target: x86_64-apple-darwin 21 | - os: ubuntu-latest 22 | target: x86_64-unknown-linux-gnu 23 | - os: windows-latest 24 | target: x86_64-pc-windows-msvc 25 | binary-postfix: ".exe" 26 | - os: ubuntu-latest 27 | target: aarch64-unknown-linux-gnu 28 | - os: macos-latest 29 | target: aarch64-apple-darwin 30 | steps: 31 | - name: Checkout repository 32 | uses: actions/checkout@v4 33 | - name: Install Rust toolchain 34 | uses: dtolnay/rust-toolchain@v1 35 | with: 36 | toolchain: stable 37 | target: ${{ matrix.job.target }} 38 | - name: Enable caching 39 | uses: Swatinem/rust-cache@v2 40 | - name: Publish (dry-run) 41 | if: matrix.job.target == 'x86_64-unknown-linux-gnu' 42 | run: | 43 | cargo build --release 44 | cargo publish --dry-run --allow-dirty 45 | - name: Install cross and build 46 | if: matrix.job.target == 'aarch64-unknown-linux-gnu' 47 | run: | 48 | cargo install cross 49 | cross build --release --target ${{ matrix.job.target }} 50 | - name: Cargo build 51 | if: matrix.job.target != 'aarch64-unknown-linux-gnu' 52 | run: cargo build --release --target ${{ matrix.job.target }} 53 | - name: Compress (Unix) 54 | if: ${{ matrix.job.os != 'windows-latest' }} 55 | run: zip -j esp-generate-${{ matrix.job.target }}.zip target/${{ matrix.job.target }}/release/esp-generate${{ matrix.job.binary-postfix }} 56 | - name: Compress (Windows) 57 | if: ${{ matrix.job.os == 'windows-latest' }} 58 | run: Compress-Archive target/${{ matrix.job.target }}/release/esp-generate${{ matrix.job.binary-postfix }} esp-generate-${{ matrix.job.target }}.zip 59 | - name: Upload compressed artifact 60 | uses: svenstaro/upload-release-action@v2 61 | with: 62 | repo_token: ${{ secrets.GITHUB_TOKEN }} 63 | file: esp-generate-${{ matrix.job.target }}.zip 64 | tag: ${{ github.ref }} 65 | - name: Upload binary artifact 66 | uses: svenstaro/upload-release-action@v2 67 | with: 68 | repo_token: ${{ secrets.GITHUB_TOKEN }} 69 | file: target/${{ matrix.job.target }}/release/esp-generate${{ matrix.job.binary-postfix }} 70 | asset_name: esp-generate-${{ matrix.job.target }}${{ matrix.job.binary-postfix }} 71 | tag: ${{ github.ref }} 72 | publish-cratesio: 73 | name: Publishing to Crates.io 74 | needs: publish-artifacts 75 | runs-on: ubuntu-latest 76 | steps: 77 | - name: Checkout repository 78 | uses: actions/checkout@v4 79 | - name: Install Rust toolchain 80 | uses: dtolnay/rust-toolchain@v1 81 | with: 82 | toolchain: stable 83 | - name: Enable caching 84 | uses: Swatinem/rust-cache@v2 85 | - name: Cargo publish 86 | run: | 87 | cargo build --release 88 | cargo publish --allow-dirty --token ${{ secrets.CARGO_API_KEY }} 89 | -------------------------------------------------------------------------------- /src/template.rs: -------------------------------------------------------------------------------- 1 | use esp_metadata::Chip; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Clone, Serialize, Deserialize, Debug)] 5 | pub struct GeneratorOption { 6 | pub name: String, 7 | pub display_name: String, 8 | /// Selecting one option in the group deselect other options of the same group. 9 | #[serde(default)] 10 | pub selection_group: String, 11 | #[serde(default)] 12 | pub help: String, 13 | #[serde(default)] 14 | pub requires: Vec, 15 | #[serde(default)] 16 | pub chips: Vec, 17 | } 18 | 19 | impl GeneratorOption { 20 | pub fn options(&self) -> Vec { 21 | vec![self.name.to_string()] 22 | } 23 | } 24 | 25 | #[derive(Clone, Serialize, Deserialize, Debug)] 26 | pub struct GeneratorOptionCategory { 27 | pub name: String, 28 | pub display_name: String, 29 | #[serde(default)] 30 | pub help: String, 31 | #[serde(default)] 32 | pub requires: Vec, 33 | #[serde(default)] 34 | pub options: Vec, 35 | } 36 | 37 | impl GeneratorOptionCategory { 38 | pub fn options(&self) -> Vec { 39 | let mut res = Vec::new(); 40 | for option in self.options.iter() { 41 | res.extend(option.options()); 42 | } 43 | res 44 | } 45 | } 46 | 47 | #[derive(Clone, Serialize, Deserialize, Debug)] 48 | pub enum GeneratorOptionItem { 49 | Category(GeneratorOptionCategory), 50 | Option(GeneratorOption), 51 | } 52 | 53 | impl GeneratorOptionItem { 54 | pub fn title(&self) -> &str { 55 | match self { 56 | GeneratorOptionItem::Category(category) => category.display_name.as_str(), 57 | GeneratorOptionItem::Option(option) => option.display_name.as_str(), 58 | } 59 | } 60 | 61 | pub fn name(&self) -> &str { 62 | match self { 63 | GeneratorOptionItem::Category(category) => category.name.as_str(), 64 | GeneratorOptionItem::Option(option) => option.name.as_str(), 65 | } 66 | } 67 | 68 | pub fn options(&self) -> Vec { 69 | match self { 70 | GeneratorOptionItem::Category(category) => category.options(), 71 | GeneratorOptionItem::Option(option) => option.options(), 72 | } 73 | } 74 | 75 | pub fn is_category(&self) -> bool { 76 | matches!(self, GeneratorOptionItem::Category(_)) 77 | } 78 | 79 | pub fn chips(&self) -> &[Chip] { 80 | match self { 81 | GeneratorOptionItem::Category(_) => &[], 82 | GeneratorOptionItem::Option(option) => option.chips.as_slice(), 83 | } 84 | } 85 | 86 | pub fn requires(&self) -> &[String] { 87 | match self { 88 | GeneratorOptionItem::Category(category) => category.requires.as_slice(), 89 | GeneratorOptionItem::Option(option) => option.requires.as_slice(), 90 | } 91 | } 92 | 93 | pub fn help(&self) -> &str { 94 | match self { 95 | GeneratorOptionItem::Category(category) => &category.help, 96 | GeneratorOptionItem::Option(option) => &option.help, 97 | } 98 | } 99 | } 100 | 101 | #[derive(Clone, Serialize, Deserialize)] 102 | pub struct Template { 103 | pub options: Vec, 104 | } 105 | 106 | impl Template { 107 | pub fn all_options(&self) -> Vec<&GeneratorOption> { 108 | all_options_in(&self.options) 109 | } 110 | } 111 | 112 | fn all_options_in(options: &[GeneratorOptionItem]) -> Vec<&GeneratorOption> { 113 | options 114 | .iter() 115 | .flat_map(|o| match o { 116 | GeneratorOptionItem::Option(option) => vec![option], 117 | GeneratorOptionItem::Category(category) => all_options_in(&category.options), 118 | }) 119 | .collect() 120 | } 121 | -------------------------------------------------------------------------------- /template/src/bin/main.rs: -------------------------------------------------------------------------------- 1 | //INCLUDEFILE !option("embassy") 2 | #![no_std] 3 | #![no_main] 4 | #![deny( 5 | clippy::mem_forget, 6 | reason = "mem::forget is generally not safe to do with esp_hal types, especially those \ 7 | holding buffers for the duration of a data transfer." 8 | )] 9 | #![deny(clippy::large_stack_frames)] 10 | 11 | use esp_hal::{ 12 | clock::CpuClock, 13 | main, 14 | time::{Duration, Instant}, 15 | }; 16 | //IF option("wifi") || option("ble-bleps") 17 | use esp_hal::timer::timg::TimerGroup; 18 | //ENDIF 19 | //IF option("ble-bleps") 20 | use esp_radio::ble::controller::BleConnector; 21 | //ENDIF 22 | 23 | //IF option("defmt") 24 | //IF !option("probe-rs") 25 | //+use esp_println as _; 26 | //ENDIF 27 | //+use defmt::info; 28 | //ELIF option("log") 29 | use log::info; 30 | //ELIF option("probe-rs") // without defmt 31 | use rtt_target::rprintln; 32 | //ENDIF !defmt 33 | 34 | //IF !option("panic-handler") 35 | //+#[panic_handler] 36 | //+fn panic(_: &core::panic::PanicInfo) -> ! { 37 | //+ loop {} 38 | //+} 39 | //ELIF option("esp-backtrace") 40 | use esp_backtrace as _; 41 | //ELIF option("panic-rtt-target") 42 | //+use panic_rtt_target as _; 43 | //ENDIF 44 | 45 | //IF option("alloc") 46 | extern crate alloc; 47 | //ENDIF 48 | 49 | // This creates a default app-descriptor required by the esp-idf bootloader. 50 | // For more information see: 51 | esp_bootloader_esp_idf::esp_app_desc!(); 52 | 53 | #[allow( 54 | clippy::large_stack_frames, 55 | reason = "it's not unusual to allocate larger buffers etc. in main" 56 | )] 57 | #[main] 58 | fn main() -> ! { 59 | //REPLACE generate-version generate-version 60 | // generator version: generate-version 61 | 62 | //IF option("probe-rs") 63 | //IF option("defmt") 64 | rtt_target::rtt_init_defmt!(); 65 | //ELSE 66 | rtt_target::rtt_init_print!(); 67 | //ENDIF 68 | //ELIF option("log") 69 | esp_println::logger::init_logger_from_env(); 70 | //ENDIF 71 | 72 | let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max()); 73 | //IF option("wifi") || option("ble-bleps") 74 | let peripherals = esp_hal::init(config); 75 | //ELSE 76 | //+let _peripherals = esp_hal::init(config); 77 | //ENDIF 78 | 79 | //IF option("alloc") 80 | //REPLACE 65536 max-dram2-uninit 81 | esp_alloc::heap_allocator!(#[esp_hal::ram(reclaimed)] size: 65536); 82 | //IF option("wifi") && (option("ble-bleps") || option("ble-trouble")) 83 | // COEX needs more RAM - so we've added some more 84 | esp_alloc::heap_allocator!(size: 64 * 1024); 85 | //ENDIF 86 | //ENDIF alloc 87 | 88 | //IF option("wifi") || option("ble-bleps") 89 | let timg0 = TimerGroup::new(peripherals.TIMG0); 90 | //IF option("esp32") || option("esp32s2") || option("esp32s3") 91 | esp_rtos::start(timg0.timer0); 92 | //ELSE 93 | let sw_interrupt = 94 | esp_hal::interrupt::software::SoftwareInterruptControl::new(peripherals.SW_INTERRUPT); 95 | esp_rtos::start(timg0.timer0, sw_interrupt.software_interrupt0); 96 | //ENDIF 97 | let radio_init = esp_radio::init().expect("Failed to initialize Wi-Fi/BLE controller"); 98 | //ENDIF 99 | //IF option("wifi") 100 | let (mut _wifi_controller, _interfaces) = 101 | esp_radio::wifi::new(&radio_init, peripherals.WIFI, Default::default()) 102 | .expect("Failed to initialize Wi-Fi controller"); 103 | //ENDIF 104 | //IF option("ble-bleps") 105 | let _connector = BleConnector::new(&radio_init, peripherals.BT, Default::default()); 106 | //ENDIF 107 | 108 | loop { 109 | //IF option("defmt") || option("log") 110 | info!("Hello world!"); 111 | //ELIF option("probe-rs") // without defmt 112 | rprintln!("Hello world!"); 113 | //ENDIF 114 | let delay_start = Instant::now(); 115 | while delay_start.elapsed() < Duration::from_millis(500) {} 116 | } 117 | 118 | //REPLACE {current-version} esp-hal-version 119 | // for inspiration have a look at the examples at https://github.com/esp-rs/esp-hal/tree/esp-hal-v{current-version}/examples 120 | } 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `esp-generate` 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/esp-generate?labelColor=1C2C2E&color=C96329&logo=Rust&style=flat-square)](https://crates.io/crates/esp-generate) 4 | ![MSRV](https://img.shields.io/badge/MSRV-1.86-blue?labelColor=1C2C2E&logo=Rust&style=flat-square) 5 | ![Crates.io](https://img.shields.io/crates/l/esp-generate?labelColor=1C2C2E&style=flat-square) 6 | 7 | Template generation tool to create `no_std` applications targeting Espressif's line of SoCs and modules. 8 | 9 | At present, this template supports the ESP32, ESP32-C2/C3/C6, ESP32-H2, and ESP32-S2/S3. Support for additional devices will be added as they become available. 10 | 11 | ## Quickstart 12 | 13 | To generate a project using this template: 14 | 15 | 1. Install `esp-generate`: 16 | 17 | ``` 18 | cargo install esp-generate --locked 19 | ``` 20 | 21 | You can also directly download pre-compiled [release binaries] or use [`cargo-binstall`]. 22 | 23 | 2. Generate a project. There are two options: 24 | 25 | 1. Using the Terminal User Interface (TUI): 26 | 27 | ``` 28 | esp-generate 29 | ``` 30 | You will be prompted to select a target chip and name for your project, after which you would use TUI to select the other options you need for your project. 31 | 32 | 2. Using the Command Line Interface (CLI), adding the options to the `esp-generate` command: 33 | 34 | ``` 35 | esp-generate --chip esp32 -o alloc -o wifi your-project 36 | ``` 37 | Use the `--headless` flag to avoid using the TUI. 38 | 39 | Replace the chip and project name accordingly, and select the desired options using the `-o/--option` flag. For a full list of available options, see [Available Options](#available-options) section of this README. 40 | 41 | [release binaries]: https://github.com/esp-rs/esp-generate/releases 42 | [`cargo-binstall`]: https://github.com/cargo-bins/cargo-binstall 43 | 44 | ## Available Options 45 | 46 | - `unstable-hal`: Enables esp-hal features that may not be ready for general use yet. 47 | - `alloc`: Enables allocations via the `esp-alloc` crate. 48 | - `wifi`: Enables Wi-Fi via the `esp-radio` crate; requires `alloc`. 49 | - `ble-bleps`: Enables BLE via the `esp-radio` crate using `bleps`; requires `alloc`, mutually exclusive with `ble-trouble`. 50 | - `ble-trouble`: Enables BLE via the `esp-radio`crate using `embassy-trouble`; requires `alloc`, mutually exclusive with `ble-bleps`. 51 | - `embassy`: Adds `embassy` framework support. 52 | - `stack-smashing-protection`: Enables [stack smashing protection](https://doc.rust-lang.org/rustc/exploit-mitigations.html#stack-smashing-protection). Requires nightly Rust. 53 | - `probe-rs`: Replaces `espflash` with `probe-rs` and enables RTT-based options. 54 | - `flashing-probe-rs`: Contains options that require `probe-rs`: 55 | - `defmt`: Adds support for `defmt` printing. Uses `rtt-target` as the RTT implementation. 56 | - `panic-rtt-target`: Uses `panic-rtt-target` as the panic handler. 57 | - `embedded-test`: Enables `embedded-test` support and generates a simple demo test case. 58 | - `flashing-espflash`: Contains options that require `espflash`: 59 | - `log`: Uses the `log` library to print messages. 60 | - `defmt`: Adds support for `defmt` printing. Uses `esp-println` and configures `espflash` to decode `defmt` logs. 61 | - `esp-backtrace`: Uses `esp-backtrace` as the panic handler. 62 | - `optional`: Enables the following set of options: 63 | - `wokwi`: Adds support for Wokwi simulation using [VS Code Wokwi extension]. 64 | - `ci` Adds GitHub Actions support with some basics checks. 65 | - `editors`: Select the editor integrations: 66 | - `helix`: The Helix editor 67 | - `neovim`: Neovim 68 | - `vscode`: Visual Studio Code 69 | - `zed`: The Zed editor 70 | 71 | [VS Code Wokwi extension]: https://marketplace.visualstudio.com/items?itemName=wokwi.wokwi-vscode 72 | 73 | ## License 74 | 75 | Licensed under either of: 76 | 77 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 78 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 79 | 80 | at your option. 81 | 82 | ### Contribution 83 | 84 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in 85 | the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without 86 | any additional terms or conditions. 87 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | types: [opened, synchronize, reopened] 5 | paths-ignore: 6 | - "**/README.md" 7 | merge_group: 8 | schedule: 9 | - cron: "34 2 * * *" 10 | workflow_dispatch: 11 | inputs: 12 | build: 13 | description: "Fully build the tested configurations" 14 | required: true 15 | type: boolean 16 | all_combinations: 17 | description: "Checks all combinations of options" 18 | required: true 19 | type: boolean 20 | 21 | env: 22 | CARGO_TARGET_DIR: ${{ github.workspace }}/target 23 | CARGO_TERM_COLOR: always 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | MSRV: "1.86" 26 | SSID: "" 27 | PASSWORD: "" 28 | 29 | # Cancel any currently running workflows from the same PR, branch, or 30 | # tag when a new workflow is triggered. 31 | # 32 | # https://stackoverflow.com/a/66336834 33 | concurrency: 34 | cancel-in-progress: true 35 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 36 | 37 | jobs: 38 | # -------------------------------------------------------------------------- 39 | # Verify 40 | 41 | verify: 42 | name: "Check ${{ matrix.chip }}" 43 | runs-on: ubuntu-latest 44 | 45 | strategy: 46 | fail-fast: false 47 | matrix: 48 | chip: [esp32, esp32c2, esp32c3, esp32c6, esp32h2, esp32s2, esp32s3] 49 | 50 | steps: 51 | - uses: actions/checkout@v4 52 | 53 | # Rust toolchain for Xtensa: 54 | - if: ${{ contains(fromJson('["esp32", "esp32s2", "esp32s3"]'), matrix.chip) }} 55 | uses: esp-rs/xtensa-toolchain@v1.6 56 | with: 57 | default: true 58 | buildtargets: ${{ matrix.chip }} 59 | ldproxy: false 60 | 61 | # Rust toolchain for RISC-V: 62 | - if: ${{ !contains(fromJson('["esp32", "esp32s2", "esp32s3"]'), matrix.chip) }} 63 | uses: dtolnay/rust-toolchain@stable 64 | with: 65 | target: riscv32imc-unknown-none-elf,riscv32imac-unknown-none-elf 66 | components: clippy,rustfmt,rust-src 67 | 68 | # Rust toolchain for RISC-V: 69 | - if: ${{ !contains(fromJson('["esp32", "esp32s2", "esp32s3"]'), matrix.chip) }} 70 | uses: dtolnay/rust-toolchain@nightly 71 | with: 72 | target: riscv32imc-unknown-none-elf,riscv32imac-unknown-none-elf 73 | components: clippy,rustfmt,rust-src 74 | 75 | # //Define a new environment variable called toolchain 76 | - if: ${{ contains(fromJson('["esp32", "esp32s2", "esp32s3"]'), matrix.chip) }} 77 | run: echo "TOOLCHAIN=+esp" >> $GITHUB_ENV 78 | 79 | - uses: Swatinem/rust-cache@v2 80 | 81 | - name: Generate and check project 82 | run: cargo ${{ env.TOOLCHAIN }} xtask check ${{ matrix.chip }} ${{ fromJSON('["", "--all-combinations"]')[inputs.all_combinations || github.event_name == 'schedule'] }} ${{ fromJSON('["", "--build"]')[inputs.build || github.event_name == 'schedule'] }} 83 | 84 | - if: github.event_name == 'schedule' 85 | name: Run cargo-package 86 | run: cargo package --allow-dirty 87 | 88 | - if: ${{ github.event_name == 'schedule' && failure() }} 89 | name: Create or Update GitHub Issue 90 | env: 91 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 92 | run: | 93 | sudo apt-get install gh -y 94 | ISSUE_NAME=$(gh issue list --state open --search "Scheduled CI Failure in:title" --json number --jq '.[0].number') 95 | 96 | if [[ -z "$ISSUE_NAME" ]]; 97 | then 98 | gh issue create \ 99 | --title "Scheduled CI Failure" \ 100 | --body "Scheduled CI Workflow Failed! [View the failed job](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})." 101 | fi 102 | 103 | # -------------------------------------------------------------------------- 104 | # Test 105 | 106 | test: 107 | runs-on: ubuntu-latest 108 | 109 | steps: 110 | - uses: actions/checkout@v4 111 | - uses: dtolnay/rust-toolchain@stable 112 | 113 | - name: Run tests 114 | run: cargo test 115 | 116 | # -------------------------------------------------------------------------- 117 | # Lint 118 | 119 | clippy: 120 | runs-on: ubuntu-latest 121 | 122 | steps: 123 | - uses: actions/checkout@v4 124 | - uses: dtolnay/rust-toolchain@stable 125 | with: 126 | components: clippy 127 | 128 | - name: Run clippy 129 | run: cargo clippy -- -D warnings 130 | 131 | # -------------------------------------------------------------------------- 132 | # MSRV 133 | 134 | msrv: 135 | runs-on: ubuntu-latest 136 | 137 | steps: 138 | - uses: actions/checkout@v4 139 | 140 | - uses: dtolnay/rust-toolchain@v1 141 | with: 142 | toolchain: ${{ env.MSRV }} 143 | - run: cargo check -p esp-generate 144 | -------------------------------------------------------------------------------- /template/src/bin/async_main.rs: -------------------------------------------------------------------------------- 1 | //INCLUDEFILE option("embassy") 2 | //INCLUDE_AS src/bin/main.rs 3 | #![no_std] 4 | #![no_main] 5 | #![deny( 6 | clippy::mem_forget, 7 | reason = "mem::forget is generally not safe to do with esp_hal types, especially those \ 8 | holding buffers for the duration of a data transfer." 9 | )] 10 | #![deny(clippy::large_stack_frames)] 11 | 12 | use esp_hal::clock::CpuClock; 13 | use esp_hal::timer::timg::TimerGroup; 14 | 15 | //IF option("ble-trouble") || option("ble-bleps") 16 | use esp_radio::ble::controller::BleConnector; 17 | //ENDIF 18 | //IF option("ble-trouble") 19 | use bt_hci::controller::ExternalController; 20 | use trouble_host::prelude::*; 21 | //ENDIF 22 | 23 | //IF option("defmt") 24 | //IF !option("probe-rs") 25 | //+use esp_println as _; 26 | //ENDIF 27 | //+use defmt::info; 28 | //ELIF option("log") 29 | use log::info; 30 | //ELIF option("probe-rs") // without defmt 31 | use rtt_target::rprintln; 32 | //ENDIF !defmt 33 | 34 | use embassy_executor::Spawner; 35 | use embassy_time::{Duration, Timer}; 36 | 37 | //IF !option("panic-handler") 38 | //+#[panic_handler] 39 | //+fn panic(_: &core::panic::PanicInfo) -> ! { 40 | //+ loop {} 41 | //+} 42 | //ELIF option("esp-backtrace") 43 | use esp_backtrace as _; 44 | //ELIF option("panic-rtt-target") 45 | //+use panic_rtt_target as _; 46 | //ENDIF 47 | 48 | //IF option("alloc") 49 | extern crate alloc; 50 | //ENDIF 51 | 52 | //IF option("ble-trouble") 53 | const CONNECTIONS_MAX: usize = 1; 54 | const L2CAP_CHANNELS_MAX: usize = 1; 55 | //ENDIF 56 | 57 | // This creates a default app-descriptor required by the esp-idf bootloader. 58 | // For more information see: 59 | esp_bootloader_esp_idf::esp_app_desc!(); 60 | 61 | #[allow( 62 | clippy::large_stack_frames, 63 | reason = "it's not unusual to allocate larger buffers etc. in main" 64 | )] 65 | #[esp_rtos::main] 66 | async fn main(spawner: Spawner) -> ! { 67 | //REPLACE generate-version generate-version 68 | // generator version: generate-version 69 | 70 | //IF option("probe-rs") 71 | //IF option("defmt") 72 | rtt_target::rtt_init_defmt!(); 73 | //ELSE 74 | rtt_target::rtt_init_print!(); 75 | //ENDIF 76 | //ELIF option("log") 77 | esp_println::logger::init_logger_from_env(); 78 | //ENDIF 79 | 80 | let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max()); 81 | let peripherals = esp_hal::init(config); 82 | 83 | //IF option("alloc") 84 | //REPLACE 65536 max-dram2-uninit 85 | esp_alloc::heap_allocator!(#[esp_hal::ram(reclaimed)] size: 65536); 86 | //IF option("wifi") && (option("ble-bleps") || option("ble-trouble")) 87 | // COEX needs more RAM - so we've added some more 88 | esp_alloc::heap_allocator!(size: 64 * 1024); 89 | //ENDIF 90 | //ENDIF alloc 91 | 92 | let timg0 = TimerGroup::new(peripherals.TIMG0); 93 | //IF option("esp32") || option("esp32s2") || option("esp32s3") 94 | esp_rtos::start(timg0.timer0); 95 | //ELSE 96 | let sw_interrupt = 97 | esp_hal::interrupt::software::SoftwareInterruptControl::new(peripherals.SW_INTERRUPT); 98 | esp_rtos::start(timg0.timer0, sw_interrupt.software_interrupt0); 99 | //ENDIF 100 | 101 | //IF option("defmt") || option("log") 102 | info!("Embassy initialized!"); 103 | //ELIF option("probe-rs") // without defmt 104 | rprintln!("Embassy initialized!"); 105 | //ENDIF 106 | 107 | //IF option("ble-trouble") || option("ble-bleps") || option("wifi") 108 | let radio_init = esp_radio::init().expect("Failed to initialize Wi-Fi/BLE controller"); 109 | //ENDIF 110 | //IF option("wifi") 111 | let (mut _wifi_controller, _interfaces) = 112 | esp_radio::wifi::new(&radio_init, peripherals.WIFI, Default::default()) 113 | .expect("Failed to initialize Wi-Fi controller"); 114 | //ENDIF 115 | //IF option("ble-trouble") 116 | // find more examples https://github.com/embassy-rs/trouble/tree/main/examples/esp32 117 | let transport = BleConnector::new(&radio_init, peripherals.BT, Default::default()).unwrap(); 118 | let ble_controller = ExternalController::<_, 1>::new(transport); 119 | let mut resources: HostResources = 120 | HostResources::new(); 121 | let _stack = trouble_host::new(ble_controller, &mut resources); 122 | //ELIF option("ble-bleps") 123 | let _connector = BleConnector::new(&radio_init, peripherals.BT, Default::default()); 124 | //ENDIF 125 | 126 | // TODO: Spawn some tasks 127 | let _ = spawner; 128 | 129 | loop { 130 | //IF option("defmt") || option("log") 131 | info!("Hello world!"); 132 | //ELIF option("probe-rs") // without defmt 133 | rprintln!("Hello world!"); 134 | //ENDIF 135 | Timer::after(Duration::from_secs(1)).await; 136 | } 137 | 138 | //REPLACE {current-version} esp-hal-version {current-version} esp-hal-version 139 | // for inspiration have a look at the examples at https://github.com/esp-rs/esp-hal/tree/esp-hal-v{current-version}/examples 140 | } 141 | -------------------------------------------------------------------------------- /src/cargo.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use toml_edit::{DocumentMut, Item, Value}; 4 | 5 | type Result = std::result::Result>; 6 | 7 | pub struct CargoToml { 8 | pub manifest: toml_edit::DocumentMut, 9 | } 10 | 11 | const DEPENDENCY_KINDS: [&str; 3] = ["dependencies", "dev-dependencies", "build-dependencies"]; 12 | 13 | impl CargoToml { 14 | pub fn load(manifest: &str) -> Result { 15 | // Parse the manifest string into a mutable TOML document. 16 | Ok(Self { 17 | manifest: manifest.parse::()?, 18 | }) 19 | } 20 | 21 | pub fn is_published(&self) -> bool { 22 | // Check if the package is published by looking for the `publish` key 23 | // in the manifest. 24 | let Item::Table(package) = &self.manifest["package"] else { 25 | unreachable!("The package table is missing in the manifest"); 26 | }; 27 | 28 | let Some(publish) = package.get("publish") else { 29 | return true; 30 | }; 31 | 32 | publish.as_bool().unwrap_or(true) 33 | } 34 | 35 | pub fn version(&self) -> &str { 36 | self.manifest["package"]["version"] 37 | .as_str() 38 | .unwrap() 39 | .trim() 40 | .trim_matches('"') 41 | } 42 | 43 | pub fn msrv(&self) -> &str { 44 | self.manifest["package"]["rust-version"] 45 | .as_str() 46 | .unwrap() 47 | .trim() 48 | .trim_matches('"') 49 | } 50 | 51 | /// Calls a callback for each table that contains dependencies. 52 | /// 53 | /// Callback arguments: 54 | /// - `path`: The path to the table (e.g. `dependencies.package`) 55 | /// - `dependency_kind`: The kind of dependency (e.g. `dependencies`, 56 | /// `dev-dependencies`) 57 | /// - `table`: The table itself 58 | pub fn visit_dependencies( 59 | &self, 60 | mut handle_dependencies: impl FnMut(&str, &'static str, &toml_edit::Table), 61 | ) { 62 | fn recurse_dependencies( 63 | path: String, 64 | table: &toml_edit::Table, 65 | handle_dependencies: &mut impl FnMut(&str, &'static str, &toml_edit::Table), 66 | ) { 67 | // Walk through tables recursively so that we can find *all* dependencies. 68 | for (key, item) in table.iter() { 69 | if let Item::Table(table) = item { 70 | let path = if path.is_empty() { 71 | key.to_string() 72 | } else { 73 | format!("{path}.{key}") 74 | }; 75 | recurse_dependencies(path, table, handle_dependencies); 76 | } 77 | } 78 | for dependency_kind in DEPENDENCY_KINDS { 79 | let Some(Item::Table(table)) = table.get(dependency_kind) else { 80 | continue; 81 | }; 82 | 83 | handle_dependencies(&path, dependency_kind, table); 84 | } 85 | } 86 | 87 | recurse_dependencies( 88 | String::new(), 89 | self.manifest.as_table(), 90 | &mut handle_dependencies, 91 | ); 92 | } 93 | 94 | pub fn dependency_version(&self, package_name: &str) -> String { 95 | let mut dep_version = String::new(); 96 | self.visit_dependencies(|_, _, table| { 97 | // Update dependencies which specify a version: 98 | if !table.contains_key(package_name) { 99 | return; 100 | } 101 | match &table[package_name] { 102 | Item::Value(Value::String(value)) => { 103 | // package = "version" 104 | dep_version = value.value().to_string(); 105 | } 106 | Item::Table(table) if table.contains_key("version") => { 107 | // [package] 108 | // version = "version" 109 | dep_version = table["version"].as_value().unwrap().to_string(); 110 | } 111 | Item::Value(Value::InlineTable(table)) if table.contains_key("version") => { 112 | // package = { version = "version" } 113 | dep_version = table["version"].as_str().unwrap().to_string(); 114 | } 115 | Item::None => { 116 | // alias = { package = "foo", version = "version" } 117 | let update_renamed_dep = table.get_values().iter().find_map(|(k, p)| { 118 | if let Value::InlineTable(table) = p { 119 | if let Some(Value::String(name)) = &table.get("package") { 120 | if name.value() == package_name { 121 | // Return the actual key of this dependency, e.g.: 122 | // `procmacros = { package = "esp-hal-procmacros" }` 123 | // ^^^^^^^^^^ 124 | return Some(k.last().unwrap().get().to_string()); 125 | } 126 | } 127 | } 128 | 129 | None 130 | }); 131 | 132 | if let Some(dependency_name) = update_renamed_dep { 133 | dep_version = table[&dependency_name]["version"] 134 | .as_value() 135 | .unwrap() 136 | .to_string(); 137 | } 138 | } 139 | _ => {} 140 | } 141 | }); 142 | 143 | dep_version.trim_start_matches('=').to_string() 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /template/template.yaml: -------------------------------------------------------------------------------- 1 | #INCLUDEFILE false 2 | options: 3 | - !Option 4 | name: unstable-hal 5 | display_name: Enable unstable HAL features. 6 | help: "This configuration enables unstable esp-hal features. 7 | These come with no stability guarantees, and could be changed or removed at any time." 8 | 9 | - !Option 10 | name: alloc 11 | display_name: Enable allocations via the esp-alloc crate. 12 | help: esp-alloc comes with no stability guarantees at this time. 13 | 14 | - !Option 15 | name: wifi 16 | display_name: Enable Wi-Fi via the esp-radio crate. 17 | help: esp-radio comes with no stability guarantees at this time. 18 | requires: 19 | - alloc 20 | - unstable-hal 21 | chips: 22 | - esp32 23 | - esp32c2 24 | - esp32c3 25 | - esp32c6 26 | - esp32s2 27 | - esp32s3 28 | 29 | - !Option 30 | name: ble-bleps 31 | display_name: Enable BLE via the esp-radio crate (bleps). 32 | help: esp-radio comes with no stability guarantees at this time. 33 | selection_group: ble-lib 34 | requires: 35 | - alloc 36 | - unstable-hal 37 | chips: 38 | - esp32 39 | - esp32c2 40 | - esp32c3 41 | - esp32c6 42 | - esp32h2 43 | - esp32s3 44 | 45 | - !Option 46 | name: ble-trouble 47 | display_name: Enable BLE via the esp-radio crate (embassy-trouble). 48 | help: esp-radio comes with no stability guarantees at this time. 49 | selection_group: ble-lib 50 | requires: 51 | - alloc 52 | - unstable-hal 53 | - embassy 54 | chips: 55 | - esp32 56 | - esp32c2 57 | - esp32c3 58 | - esp32c6 59 | - esp32h2 60 | - esp32s3 61 | 62 | - !Option 63 | name: embassy 64 | display_name: Add embassy framework support. 65 | help: esp-rtos module responsible for embassy support comes with no stability guarantees at this time. 66 | selection_group: base-template 67 | requires: 68 | - unstable-hal 69 | 70 | - !Option 71 | name: stack-smashing-protection 72 | display_name: Enable stack smashing protection. 73 | help: Requires nightly Rust. Note that this option generates additional checks in most functions 74 | and will slow down your code. 75 | 76 | - !Option 77 | name: probe-rs 78 | display_name: Use probe-rs to flash and monitor instead of espflash. 79 | help: probe-rs is a debugger that connects to the chips over JTAG. It can be used to flash and 80 | monitor, and it can also be used to interactively debug an application, or run tests on the 81 | hardware. Semihosting or RTT-based technologies like defmt-rtt require probe-rs. 82 | chips: 83 | - esp32c6 84 | - esp32h2 85 | - esp32s3 86 | 87 | - !Option 88 | name: probe-rs 89 | display_name: Use probe-rs to flash and monitor instead of espflash. 90 | help: probe-rs is a debugger that connects to the chips over JTAG. It can be used to flash and 91 | monitor, and it can also be used to interactively debug an application, or run tests on the 92 | hardware. Semihosting or RTT-based technologies like defmt-rtt require probe-rs. 93 | 94 | probe-rs requires a debug probe like esp-prog, and will not work with USB-UART adapters that 95 | often come on development boards. 96 | chips: 97 | - esp32 98 | - esp32s2 99 | - esp32c2 100 | - esp32c3 101 | 102 | - !Category 103 | name: flashing-probe-rs 104 | display_name: Flashing, logging and debugging (probe-rs) 105 | requires: 106 | - probe-rs 107 | options: 108 | - !Option 109 | name: defmt 110 | display_name: Use defmt to print messages. 111 | selection_group: log-frontend 112 | - !Option 113 | name: panic-rtt-target 114 | display_name: Use panic-rtt-target as the panic handler. 115 | selection_group: panic-handler 116 | requires: 117 | - probe-rs 118 | - !Option 119 | name: embedded-test 120 | display_name: Enable embedded-test support. 121 | requires: 122 | - probe-rs 123 | 124 | - !Category 125 | name: flashing-espflash 126 | display_name: Flashing, logging and debugging (espflash) 127 | requires: 128 | - "!probe-rs" 129 | options: 130 | - !Option 131 | name: log 132 | display_name: Use the log crate to print messages. 133 | selection_group: log-frontend 134 | requires: 135 | - "!probe-rs" 136 | - !Option 137 | name: defmt 138 | display_name: Use defmt to print messages. 139 | selection_group: log-frontend 140 | - !Option 141 | name: esp-backtrace 142 | display_name: Use esp-backtrace as the panic handler. 143 | selection_group: panic-handler 144 | requires: 145 | - "!probe-rs" 146 | 147 | - !Category 148 | name: optional 149 | display_name: Options 150 | options: 151 | - !Option 152 | name: wokwi 153 | display_name: Add support for Wokwi simulation using VS Code Wokwi extension. 154 | chips: 155 | - esp32 156 | - esp32c3 157 | - esp32c6 158 | - esp32h2 159 | - esp32s2 160 | - esp32s3 161 | 162 | - !Option 163 | name: ci 164 | display_name: Add GitHub Actions support with some basic checks. 165 | 166 | - !Category 167 | name: editor 168 | display_name: Optional editor integration 169 | options: 170 | - !Option 171 | name: helix 172 | display_name: Add settings for Helix Editor 173 | 174 | - !Option 175 | name: neovim 176 | display_name: Add settings for Neovim 177 | 178 | - !Option 179 | name: vscode 180 | display_name: Add settings for Visual Studio Code 181 | 182 | - !Option 183 | name: zed 184 | display_name: Add settings for Zed 185 | 186 | - !Category 187 | name: toolchain 188 | display_name: Rust toolchain 189 | options: 190 | - !Option 191 | name: PLACEHOLDER 192 | display_name: "" 193 | selection_group: toolchain 194 | -------------------------------------------------------------------------------- /template/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | #REPLACE project-name project-name 3 | name = "project-name" 4 | version = "0.1.0" 5 | edition = "2024" 6 | rust-version = "1.88" 7 | 8 | [[bin]] 9 | #REPLACE project-name project-name 10 | name = "project-name" 11 | path = "./src/bin/main.rs" 12 | #IF option("embedded-test") 13 | test = false 14 | 15 | [[test]] 16 | name = "hello_test" 17 | harness = false 18 | 19 | [lib] 20 | test = false 21 | #ENDIF 22 | 23 | [dependencies] 24 | esp-hal = { version = "~1.0", features = [ 25 | #REPLACE esp32c6 mcu 26 | "esp32c6", 27 | #IF option("unstable-hal") 28 | "unstable", 29 | #ENDIF 30 | #IF option("defmt") 31 | #+"defmt", 32 | #ENDIF 33 | #IF option("log") 34 | "log-04", 35 | #ENDIF 36 | ] } 37 | 38 | #IF option("embassy") || option("wifi") || option("ble-bleps") || option("ble-trouble") 39 | esp-rtos = { version = "0.2.0", features = [ 40 | #IF option("wifi") || option("ble-bleps") || option("ble-trouble") 41 | "esp-radio", 42 | #ENDIF 43 | #IF option("alloc") 44 | "esp-alloc", 45 | #ENDIF 46 | #IF option("embassy") 47 | "embassy", 48 | #ENDIF 49 | #REPLACE esp32c6 mcu 50 | "esp32c6", 51 | #IF option("defmt") 52 | #+"defmt", 53 | #ENDIF 54 | #IF option("log") 55 | "log-04", 56 | #ENDIF 57 | ]} 58 | #ENDIF 59 | 60 | esp-bootloader-esp-idf = { version = "0.4.0", features = [ 61 | #IF option("defmt") 62 | #+"defmt", 63 | #ENDIF 64 | #IF option("log") 65 | "log-04", 66 | #ENDIF 67 | #REPLACE esp32c6 mcu 68 | "esp32c6", 69 | ] } 70 | #IF option("defmt") 71 | #+defmt = "1.0.1" 72 | #ELIF option("log") 73 | log = "0.4.27" 74 | #ENDIF 75 | 76 | #IF option("log-frontend") || option("panic-handler") || option("probe-rs") 77 | #IF option("probe-rs") 78 | #IF option("defmt") 79 | rtt-target = { version = "0.6.2", features = ["defmt"] } 80 | #ELSE 81 | #+rtt-target = "0.6.2" 82 | #ENDIF defmt 83 | #ELSE probe-rs 84 | esp-println = { version = "0.16.1", features = [ 85 | #REPLACE esp32c6 mcu 86 | "esp32c6", 87 | #IF option("defmt") 88 | #+"defmt-espflash", 89 | #ENDIF 90 | #IF option("log") 91 | "log-04", 92 | #ENDIF 93 | ] } 94 | #ENDIF probe-rs 95 | #ENDIF log-frontend || panic-handler || probe-rs 96 | #IF option("esp-backtrace") 97 | esp-backtrace = { version = "0.18.1", features = [ 98 | #REPLACE esp32c6 mcu 99 | "esp32c6", 100 | "panic-handler", 101 | #IF option("defmt") 102 | #+"defmt", 103 | #ELSE 104 | "println", 105 | #ENDIF 106 | ] } 107 | #ELIF option("panic-rtt-target") 108 | #IF option("defmt") 109 | #+panic-rtt-target = { version = "0.2.0", features = ["defmt"] } 110 | #ELSE 111 | panic-rtt-target = "0.2.0" 112 | #ENDIF defmt 113 | #ENDIF esp-backtrace 114 | #IF option("alloc") 115 | #IF option("defmt") 116 | #+esp-alloc = { version = "0.9.0", features = ["defmt"] } 117 | #ELSE 118 | esp-alloc = "0.9.0" 119 | #ENDIF defmt 120 | #ENDIF alloc 121 | #IF option("wifi") || option("ble-bleps") || option("ble-trouble") 122 | #IF option("defmt") 123 | #+embedded-io = { version = "0.7.1", features = ["defmt"] } 124 | #ELSE 125 | embedded-io = "0.7.1" 126 | #ENDIF defmt 127 | #IF option("embassy") 128 | #IF option("defmt") 129 | #+embedded-io-async = { version = "0.7.0", features = ["defmt"] } 130 | #ELSE 131 | embedded-io-async = "0.7.0" 132 | #ENDIF defmt 133 | #IF option("wifi") 134 | embassy-net = { version = "0.7.1", features = [ 135 | "tcp", 136 | "udp", 137 | "dhcpv4", 138 | "medium-ethernet", 139 | #IF option("defmt") 140 | #+"defmt", 141 | #ENDIF 142 | #IF option("log") 143 | "log", 144 | #ENDIF 145 | ] } 146 | # for more networking protocol support see https://crates.io/crates/edge-net 147 | #ENDIF wifi 148 | #ENDIF embassy 149 | #IF option("wifi") 150 | smoltcp = { version = "0.12.0", default-features = false, features = [ 151 | "medium-ethernet", 152 | "multicast", 153 | "proto-dhcpv4", 154 | "proto-dns", 155 | "proto-ipv4", 156 | "socket-dns", 157 | "socket-raw", 158 | "socket-tcp", 159 | "socket-udp", 160 | "socket-icmp", 161 | #IF option("defmt") 162 | #+"defmt", 163 | #ENDIF 164 | #IF option("log") 165 | "log", 166 | #ENDIF 167 | ] } 168 | #ENDIF wifi 169 | esp-radio = { version = "0.17.0", features = [ 170 | #REPLACE esp32c6 mcu 171 | "esp32c6", 172 | #IF option("wifi") 173 | "wifi", 174 | "smoltcp", 175 | #ENDIF 176 | #IF option("ble-bleps") || option("ble-trouble") 177 | "ble", 178 | #ENDIF 179 | #IF option("wifi") && (option("ble-bleps") || option("ble-trouble")) 180 | "coex", 181 | #ENDIF 182 | #IF option("alloc") 183 | "esp-alloc", 184 | #ENDIF 185 | "unstable", 186 | #IF option("defmt") 187 | #+"defmt", 188 | #ENDIF 189 | #IF option("log") 190 | "log-04", 191 | #ENDIF 192 | ] } 193 | #IF option("ble-bleps") 194 | #+bleps = { git = "https://github.com/bjoernQ/bleps", package = "bleps", rev = "a5148d8ae679e021b78f53fd33afb8bb35d0b62e", features = [ "macros", "async"] } 195 | #ENDIF 196 | #IF option("ble-trouble") 197 | #+trouble-host = { version = "0.5.0", features = ["gatt"] } 198 | #+bt-hci = "0.6.0" 199 | #ENDIF 200 | #ENDIF wifi || ble || ble-trouble 201 | #IF option("embassy") 202 | embassy-executor = { version = "0.9.1", features = [ 203 | #IF option("defmt") 204 | #+"defmt", 205 | #ENDIF 206 | #IF option("log") 207 | "log", 208 | #ENDIF 209 | ] } 210 | #IF option("defmt") 211 | #+embassy-time = { version = "0.5.0", features = ["defmt"] } 212 | #ENDIF 213 | #IF option("log") 214 | embassy-time = { version = "0.5.0", features = ["log"] } 215 | #ENDIF 216 | #IF !option("defmt") && !option("log") 217 | #+embassy-time = "0.5.0" 218 | #ENDIF 219 | 220 | static_cell = "2.1.1" 221 | #ENDIF 222 | critical-section = "1.2.0" 223 | 224 | #IF option("embedded-test") 225 | [dev-dependencies] 226 | embedded-test = { version = "0.7.0", features = [ 227 | #IF option("xtensa") 228 | "xtensa-semihosting", 229 | #ENDIF 230 | #IF option("embassy") 231 | "embassy", 232 | "external-executor", 233 | #ENDIF 234 | #IF option("defmt") 235 | #+"defmt", 236 | #ENDIF 237 | #IF option("log") 238 | "log-04", 239 | #ENDIF 240 | ] } 241 | #ENDIF embedded-test 242 | 243 | [profile.dev] 244 | # Rust debug is too slow. 245 | # For debug builds always builds with some optimization 246 | opt-level = "s" 247 | 248 | [profile.release] 249 | codegen-units = 1 # LLVM can perform better optimizations using a single thread 250 | debug = 2 251 | debug-assertions = false 252 | incremental = false 253 | lto = 'fat' 254 | opt-level = 's' 255 | overflow-checks = false 256 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ### Added 11 | 12 | ### Changed 13 | 14 | ### Fixed 15 | 16 | ### Removed 17 | 18 | ## [1.1.0] - 2025-12-11 19 | 20 | ### Added 21 | 22 | - Enable https://rust-lang.github.io/rust-clippy/master/index.html#large_stack_frames with a threshold of 1024 (#241) 23 | - Add an option for custom toolchain and interactive toolchain selection (#268) 24 | 25 | ### Fixed 26 | 27 | - `"rttEnabled": true` is now set even if `defmt` is not enabled (#255) 28 | - Fix examples link in `main.rs` (#258) 29 | 30 | ## [1.0.1] - 2025-11-05 31 | 32 | ### Changed 33 | 34 | - Update `embedded-test` dependency to 0.7.0 (#251) 35 | - Use `esp_hal::ram(reclaimed)` attribute for heap allocator (#252) 36 | 37 | ### Fixed 38 | 39 | - Add App Descriptor macro to tests (#251) 40 | - ESP32 `dram2` size (#252) 41 | 42 | ## [1.0.0] - 2025-10-30 43 | 44 | ### Added 45 | 46 | - Generate settings for Neovim (#246) 47 | 48 | ### Changed 49 | 50 | - Helix config: set cargo.allTargets to false (#247) 51 | - Updated dependencies for esp-hal@1.0.0 releases (#248) 52 | 53 | ## [0.6.0] - 2025-10-14 54 | 55 | ### Added 56 | 57 | - Add Wi-Fi/BLE setup code to the blocking template (#233) 58 | - Check for the `esp-config` tool (#221) 59 | 60 | ### Changed 61 | 62 | - Update bt-hci and trouble-host dependencies 63 | - The generated project now uses Rust 2024 (#233) 64 | - The generated project now uses all of `.dram2_uninit` for its heap (#234) 65 | - Update templates and their dependenies to meet `rc1` of `esp-hal` (#237) 66 | - Bump MSRV in templates to 1.88 (#237) 67 | 68 | ### Fixed 69 | 70 | - Fix cases where padding overflow caused panic if terminal size changed (#228) 71 | 72 | ### Removed 73 | 74 | - Remove devcontainer support (#425) 75 | 76 | ## [0.5.0] - 2025-07-16 77 | 78 | ### Added 79 | 80 | - Added interactive flow option instead of setting target chip and name in arguments (#196) 81 | - Added `rust-version` to the generated Cargo.toml (#192) 82 | - Generate settings for Zed (#200) 83 | - Updated dependencies for new esp-hal 1.0.0-rc.0 release (#215) 84 | 85 | ### Changed 86 | 87 | - The generated project no longer enables `static_cell/nightly` (#199) 88 | - Use `anyhow::bail` instead of log + exit (#204) 89 | 90 | ### Fixed 91 | 92 | - Test source is no longer generated if not needed (#201) 93 | - Conflicting options specified via `-o` are now rejected (#213) 94 | 95 | ### Removed 96 | 97 | ## [0.4.0] - 2025-06-06 98 | 99 | ### Added 100 | 101 | - Added option to enable Stack smashing protection (#141) 102 | - Enabling `probe-rs` and `vscode` together now generates `.vscode/launch.json` (#143) 103 | - Provide hint when esp-wifi has no scheduler (#145) 104 | - Generate a simple `embedded-test` test suite (#144) 105 | - Enable `esp-wifi/smoltcp` when the dependency is present (#146) 106 | - Enable `defmt` or `log` on all crates that know them (#148) 107 | - The tool now prints the selected options (#154) 108 | - Enable the `clippy::mem_forget` lint since mem::forget is generally not safe to use with esp-hal. (#161) 109 | - Added option to enable integration with the `trouble` BLE library (#179) 110 | - Added `esp-bootloader-esp-idf` package (#166) 111 | 112 | ### Changed 113 | 114 | - The visual style in certain terminals no longer uses emojis (#173) 115 | - Add a description to the version check output (#178) 116 | - `esp-hal` is now pinned to avoid updating to a new prerelease by accident (#186) 117 | - `esp-hal` updated and pinned to `beta.1` (#166) 118 | - MSRV bump to 1.86 (#189) 119 | 120 | ### Fixed 121 | 122 | - The generated project no longer contains `template.yaml`. (#142) 123 | - Fixed parsing version output of old `espflash`. (#152) 124 | - Specified `defmt-03` feature for `embedded-io` and `embedded-io-async`. (#157) 125 | - Fixed RTT initialization without `defmt` (#183) 126 | 127 | ### Removed 128 | 129 | - `heapless` has been removed from dependencies. (#148) 130 | 131 | ## [0.3.1] - 2025-03-03 132 | 133 | ### Fixed 134 | 135 | - The `defmt` feature of `panic-rtt-target` is now enabled when needed. (#137) 136 | 137 | ## [0.3.0] - 2025-02-24 138 | 139 | ### Added 140 | 141 | - Added a version checker that prints a warn message if not using latest esp-generate version (#87) 142 | - After generating the project the tool now checks the rust version, espflash version and probe-rs version (#88) 143 | - Be more helpful in case of common linker errors (#94) 144 | - Support for `ELIF` conditions (#96) 145 | - Display help text (#100, #103) 146 | - Added an option to enable unstable HAL features (#104) 147 | - Added support for selection groups (#119) 148 | - Added `runArgs` to DevContainer settings to allow flashing from Linux (#154) 149 | - It is now possible to select a panic handler, and log library. (#120) 150 | 151 | ### Changed 152 | - Update `probe-rs run` arguments (#90) 153 | - When using `embassy` option, `async_main.rs` file was renamed to `main.rs` (#93) 154 | - The UI no longer allows selecting options with missing requirements, and does not allow deselecting 155 | options that are required by other options. (#101) 156 | - Options can now declare negative requirements (e.g. `!alloc` can not be enabled if `alloc` is used) (#101) 157 | - Template settings are now described in a template-specific `yaml` file (#103) 158 | - Test cases are now generated from template settings (#106) 159 | - Updated and removed some unused extensions (#109, #111) 160 | - The option names are now display in the menu (#116) 161 | - Options that are not applicable to the selected chip are not shown (#116) 162 | - Inactive menu items are now colored differently (#115) 163 | - The CLI now exits with success when the user quits (#117) 164 | 165 | ### Fixed 166 | 167 | - No longer include `smoltcp` as a dependency for BLE-only configurations (#108) 168 | 169 | ### Removed 170 | 171 | - Removed `scripts/build.sh` and `scripts/flash.sh` scripts (#124) 172 | 173 | ## [0.2.2] - 2025-01-16 174 | 175 | ### Added 176 | - The resulting `Cargo.toml` is now formated with Taplo (#72) 177 | 178 | ### Changed 179 | - Update the resulting binary name (#62) 180 | - Include version of `esp-generate` in the generated code (#67) 181 | - Use `rustc-link-arg` instead of `rustc-link-arg-bin` (#67) 182 | 183 | ### Fixed 184 | - Verify the required options are provided (#65) 185 | - Use `stable` toolchain for Rust Analyzer on Xtensa targets (#69) 186 | - Added missing template substitution in `devcontainer.json` (#70) 187 | 188 | ## [0.2.1] - 2024-11-26 189 | 190 | ### Changed 191 | - Allow selecting WiFi and BLE at the same time (#60) 192 | 193 | ### Fixed 194 | - Don't deselect just selected option (#58) 195 | - Added missing init code in non-async template (#57) 196 | 197 | ## [0.2.0] - 2024-11-21 198 | 199 | ### Added 200 | - Added editor selection. Currently only helix and vscode 201 | - Before quitting the TUI, it ask for user confirmation 202 | - Show a hint where to find examples 203 | 204 | ### Changed 205 | - Remember position when entering a sub-menu to restore state on exit. 206 | - Update dependencies to latest esp-hal releases. 207 | - Use `systimer` instead of `timg` in embassy templates for all targets but ESP32 208 | 209 | ## [0.1.0] - 2024-11-07 210 | 211 | - Initial release 212 | 213 | [Unreleased]: https://github.com/esp-rs/esp-generate/compare/v1.1.0...HEAD 214 | [1.1.0]: https://github.com/esp-rs/esp-generate/compare/v1.0.1...v1.1.0 215 | [1.0.1]: https://github.com/esp-rs/esp-generate/compare/v1.0.0...v1.0.1 216 | [1.0.0]: https://github.com/esp-rs/esp-generate/compare/v0.6.0...v1.0.0 217 | [0.6.0]: https://github.com/esp-rs/esp-generate/compare/v0.5.0...v0.6.0 218 | [0.5.0]: https://github.com/esp-rs/esp-generate/compare/v0.4.0...v0.5.0 219 | [0.4.0]: https://github.com/esp-rs/esp-generate/compare/v0.3.1...v0.4.0 220 | [0.3.1]: https://github.com/esp-rs/esp-generate/compare/v0.3.0...v0.3.1 221 | [0.3.0]: https://github.com/esp-rs/esp-generate/compare/v0.2.2...v0.3.0 222 | [0.2.2]: https://github.com/esp-rs/esp-generate/releases/tag/v0.2.2 223 | [0.2.1]: https://github.com/esp-rs/esp-generate/releases/tag/v0.2.1 224 | [0.2.0]: https://github.com/esp-rs/esp-generate/releases/tag/v0.2.0 225 | [0.1.0]: https://github.com/esp-rs/esp-generate/releases/tag/v0.1.0 226 | -------------------------------------------------------------------------------- /src/toolchain.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | 3 | use anyhow::{Result, bail}; 4 | use esp_generate::template::GeneratorOptionItem; 5 | use esp_metadata::Chip; 6 | 7 | use crate::check; 8 | 9 | /// Return all installed rustup toolchains that support the given `target` 10 | /// and meet the given MSRV. 11 | /// Return all installed rustup toolchains that support the given `target` 12 | /// and meet the given MSRV. 13 | fn filter_toolchains_for(target: &str, msrv: &check::Version) -> Result> { 14 | let output = match Command::new("rustup").args(["toolchain", "list"]).output() { 15 | Ok(res) => res, 16 | Err(err) => { 17 | // unlikely to happen, how did user even get to this point if ended up here? 18 | log::warn!("Failed to run `rustup toolchain list`: {err}"); 19 | return Ok(Vec::new()); 20 | } 21 | }; 22 | 23 | if !output.status.success() { 24 | log::warn!( 25 | "`rustup toolchain list` exited with status {:?}", 26 | output.status.code() 27 | ); 28 | return Ok(Vec::new()); 29 | } 30 | 31 | let stdout = String::from_utf8_lossy(&output.stdout); 32 | 33 | let mut available = Vec::new(); 34 | 35 | for line in stdout.lines() { 36 | let line = line.trim(); 37 | if line.is_empty() { 38 | continue; 39 | } 40 | 41 | // rustup prints things like: "stable-x86_64-unknown-linux-gnu (active, default)" 42 | let Some(name) = line.split_whitespace().next() else { 43 | continue; 44 | }; 45 | 46 | if toolchain_matches_target_and_msrv(name, target, msrv) { 47 | available.push(name.to_string()); 48 | } 49 | } 50 | 51 | Ok(available) 52 | } 53 | 54 | fn toolchain_matches_target_and_msrv( 55 | name: &str, 56 | target: &str, 57 | msrv: &check::Version, 58 | ) -> bool { 59 | // check whether this toolchain's rustc knows the desired target 60 | // (rustup doesn't recognize some custom toolchains, e.g. `esp`) 61 | let output = match Command::new("rustc") 62 | .args([ 63 | format!("+{name}"), 64 | "--print".to_string(), 65 | "target-list".to_string(), 66 | ]) 67 | .output() 68 | { 69 | Ok(res) => res, 70 | Err(err) => { 71 | log::warn!("Failed to run `rustc +{name} --print target-list`: {err}"); 72 | return false; 73 | } 74 | }; 75 | 76 | if !output.status.success() { 77 | log::warn!( 78 | "`rustc +{name} --print target-list` exited with status {:?}", 79 | output.status.code() 80 | ); 81 | return false; 82 | } 83 | 84 | if !String::from_utf8_lossy(&output.stdout) 85 | .lines() 86 | .any(|l| l.trim() == target) 87 | { 88 | // target not found - skip 89 | return false; 90 | } 91 | 92 | // call `rustc + --version` and compare to `msrv` 93 | if let Some(ver) = check::get_version("rustc", &[&format!("+{name}")]) { 94 | if !ver.is_at_least(msrv) { 95 | // toolchain version is below MSRV - skip 96 | return false; 97 | } 98 | } else { 99 | log::warn!( 100 | "Failed to detect rustc version for toolchain `{name}`; skipping MSRV check" 101 | ); 102 | } 103 | 104 | true 105 | } 106 | 107 | /// Return the currently active rustup toolchain name, if any 108 | fn active_rustup_toolchain() -> Option { 109 | let output = Command::new("rustup") 110 | .args(["show", "active-toolchain"]) 111 | .output() 112 | .ok()?; 113 | 114 | if !output.status.success() { 115 | return None; 116 | } 117 | 118 | String::from_utf8_lossy(&output.stdout) 119 | .lines() 120 | .next() 121 | .and_then(|line| line.split_whitespace().next().map(|name| name.to_string())) 122 | } 123 | 124 | /// Find the `toolchain` category in `template.yaml` and replace its placeholder 125 | /// option with one option per installed rustup toolchain that supports the 126 | /// required target and MSRV. 127 | pub(crate) fn populate_toolchain_category( 128 | chip: Chip, 129 | options: &mut [GeneratorOptionItem], 130 | cli_toolchain: Option<&str>, 131 | msrv: &check::Version, 132 | ) -> Result<()> { 133 | let target = chip.target().to_string(); 134 | 135 | let mut available = filter_toolchains_for(&target, msrv)?; 136 | 137 | // for now, we should hide the generic toolchains for Xtensa (stable-*, beta-*, nightly-*). 138 | if chip.is_xtensa() { 139 | available.retain(|name| { 140 | !(name.starts_with("stable") || name.starts_with("beta") || name.starts_with("nightly")) 141 | }); 142 | } 143 | 144 | // sanity check 145 | if available.is_empty() { 146 | if let Some(cli) = cli_toolchain { 147 | if chip.is_xtensa() 148 | && (cli.starts_with("stable") 149 | || cli.starts_with("beta") 150 | || cli.starts_with("nightly")) 151 | { 152 | bail!( 153 | "Toolchain `{cli}` is not supported for Xtensa targets; \ 154 | please use different toolchain (e.g. `esp`, see https://docs.espressif.com/projects/rust/book/getting-started/toolchain.html#xtensa-devices)" 155 | ); 156 | } 157 | 158 | bail!( 159 | "Toolchain `{cli}` does not have target `{target}` installed (or no toolchain does).\ 160 | See https://docs.espressif.com/projects/rust/book/getting-started/toolchain.html" 161 | ); 162 | } 163 | log::warn!( 164 | "No rustc toolchains found that have `{target}` installed; toolchain category will stay as placeholder" 165 | ); 166 | return Ok(()); 167 | } 168 | 169 | if let Some(cli) = cli_toolchain { 170 | if !available.iter().any(|t| t == cli) { 171 | if chip.is_xtensa() 172 | && (cli.starts_with("stable") 173 | || cli.starts_with("beta") 174 | || cli.starts_with("nightly")) 175 | { 176 | bail!( 177 | "Toolchain `{cli}` is not supported for Xtensa targets; \ 178 | please use an ESP toolchain (e.g. `esp`)" 179 | ); 180 | } 181 | 182 | bail!("Toolchain `{cli}` does not have target `{target}` installed"); 183 | } 184 | // put CLI toolchain first in toolchain search in case it was provided. 185 | available.sort(); 186 | available.sort_by_key(|t| if t == cli { 0 } else { 1 }); 187 | } 188 | 189 | // get active/default toolchain to mark it properly 190 | let default = active_rustup_toolchain(); 191 | 192 | // rewrite the `toolchain` category using the placeholder option as template 193 | for item in options.iter_mut() { 194 | let GeneratorOptionItem::Category(category) = item else { 195 | continue; 196 | }; 197 | if category.name != "toolchain" { 198 | continue; 199 | } 200 | 201 | // we know exactly the template/placeholder structure, so we can just take `first` one 202 | let template_opt = match category.options.first() { 203 | Some(GeneratorOptionItem::Option(opt)) => opt.clone(), 204 | _ => { 205 | // If `template.yaml` is broken, fail loudly 206 | panic!("toolchain category must contain a placeholder !Option"); 207 | } 208 | }; 209 | 210 | // remove the placeholder, we've "scanned" it already 211 | category.options.clear(); 212 | 213 | for toolchain in &available { 214 | // copy our placeholder option (again) to populate another toolchain instead of it 215 | let mut opt = template_opt.clone(); 216 | 217 | let is_default = default.as_deref() == Some(toolchain.as_str()); 218 | 219 | opt.name = toolchain.clone(); 220 | opt.display_name = if is_default { 221 | format!("Use `{toolchain}` toolchain [default]") 222 | } else { 223 | format!("Use `{toolchain}` toolchain") 224 | }; 225 | opt.selection_group = "toolchain".to_string(); 226 | 227 | category.options.push(GeneratorOptionItem::Option(opt)); 228 | } 229 | 230 | break; 231 | } 232 | 233 | Ok(()) 234 | } 235 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2024 esp-rs 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /xtask/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashSet, 3 | path::{Path, PathBuf}, 4 | process::{Command, Stdio}, 5 | }; 6 | 7 | use anyhow::{Result, bail}; 8 | use clap::{Parser, Subcommand}; 9 | use esp_generate::{ 10 | config::{ActiveConfiguration, find_option}, 11 | template::{GeneratorOptionCategory, GeneratorOptionItem, Template}, 12 | }; 13 | use esp_metadata::Chip; 14 | use log::info; 15 | 16 | // Unfortunate hard-coded list of non-codegen options 17 | const IGNORED_CATEGORIES: &[&str] = &["editor", "optional", "toolchain"]; 18 | 19 | #[derive(Debug, Parser)] 20 | struct Cli { 21 | #[command(subcommand)] 22 | command: Commands, 23 | } 24 | 25 | #[derive(Debug, Subcommand)] 26 | enum Commands { 27 | /// Generate a project; ensure that it builds, lints pass, and that it is 28 | /// formatted correctly 29 | Check { 30 | /// Target chip to check 31 | #[arg(value_enum)] 32 | chip: Chip, 33 | /// Verify all possible options combinations 34 | #[arg(short, long)] 35 | all_combinations: bool, 36 | /// Actually build projects, instead of just checking them 37 | #[arg(short, long)] 38 | build: bool, 39 | /// Just print what would be tested 40 | #[arg(short, long)] 41 | dry_run: bool, 42 | }, 43 | } 44 | 45 | fn main() -> Result<()> { 46 | env_logger::Builder::new() 47 | .filter_module("xtask", log::LevelFilter::Info) 48 | .init(); 49 | 50 | // The directory containing the Cargo manifest for the 'xtask' package is 51 | // a subdirectory within the workspace: 52 | let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 53 | let workspace = workspace.parent().unwrap().canonicalize()?; 54 | 55 | match Cli::parse().command { 56 | Commands::Check { 57 | chip, 58 | all_combinations, 59 | build, 60 | dry_run, 61 | } => check(&workspace, chip, all_combinations, build, dry_run), 62 | } 63 | } 64 | 65 | // ---------------------------------------------------------------------------- 66 | // CHECK 67 | 68 | fn check( 69 | workspace: &Path, 70 | chip: Chip, 71 | all_combinations: bool, 72 | build: bool, 73 | dry_run: bool, 74 | ) -> Result<()> { 75 | if build { 76 | log::info!("BUILD: {chip}"); 77 | } else { 78 | log::info!("CHECK: {chip}"); 79 | } 80 | 81 | info!("Going to check"); 82 | let to_check = options_for_chip(chip, all_combinations)?; 83 | for check in &to_check { 84 | info!("\"{}\"", check.join(", ")); 85 | } 86 | 87 | if dry_run { 88 | return Ok(()); 89 | } 90 | 91 | let target_dir = 92 | PathBuf::from(std::env::var("CARGO_TARGET_DIR").unwrap_or("target".to_string())); 93 | let mut counter = 0; 94 | const PROJECT_NAME: &str = "test"; 95 | for options in to_check { 96 | counter += 1; 97 | if counter >= 100 { 98 | // don't use `cargo clean` since it will fail because it can't delete the xtask executable 99 | for f in std::fs::read_dir(&target_dir)? { 100 | let f = f?.path(); 101 | 102 | // don't fail just because we can't remove a directory or file 103 | if f.is_dir() { 104 | let _ = std::fs::remove_dir_all(f); 105 | } else { 106 | let _ = std::fs::remove_file(f); 107 | } 108 | } 109 | 110 | counter = 0; 111 | } 112 | 113 | log::info!("WITH OPTIONS: {options:?}"); 114 | 115 | // We will generate the project in a temporary directory, to avoid 116 | // making a mess when this subcommand is executed locally: 117 | let project_dir = tempfile::tempdir()?; 118 | let project_path = project_dir.path(); 119 | log::info!("PROJECT PATH: {project_path:?}"); 120 | 121 | // Generate a project targeting the specified chip and using the 122 | // specified generation options: 123 | generate(workspace, &project_path, PROJECT_NAME, chip, &options)?; 124 | 125 | // Ensure that the generated project builds without errors: 126 | let output = Command::new("cargo") 127 | .args([if build { "build" } else { "check" }, "--quiet"]) 128 | .env_remove("RUSTUP_TOOLCHAIN") 129 | .current_dir(project_path.join(PROJECT_NAME)) 130 | .stdout(Stdio::null()) 131 | .stderr(Stdio::null()) 132 | .output()?; 133 | if !output.status.success() { 134 | bail!("Failed to execute cargo check subcommand") 135 | } 136 | 137 | // Ensure that the generated test project builds also: 138 | if options.iter().any(|o| o == "embedded-test") { 139 | let output = Command::new("cargo") 140 | .args(["test", "--no-run"]) 141 | .env_remove("RUSTUP_TOOLCHAIN") 142 | .current_dir(project_path.join(PROJECT_NAME)) 143 | .stdout(Stdio::null()) 144 | .stderr(Stdio::null()) 145 | .output()?; 146 | if !output.status.success() { 147 | bail!("Failed to execute cargo test subcommand") 148 | } 149 | } 150 | 151 | // Run clippy against the generated project to check for lint errors: 152 | let output = Command::new("cargo") 153 | .args(["clippy", "--no-deps", "--", "-Dwarnings"]) 154 | .env_remove("RUSTUP_TOOLCHAIN") 155 | .current_dir(project_path.join(PROJECT_NAME)) 156 | .stdout(Stdio::null()) 157 | .stderr(Stdio::null()) 158 | .output()?; 159 | if !output.status.success() { 160 | bail!("Failed to execute cargo clippy subcommand") 161 | } 162 | 163 | // Ensure that the generated project is correctly formatted: 164 | let output = Command::new("cargo") 165 | .args(["fmt", "--", "--check"]) 166 | .env_remove("RUSTUP_TOOLCHAIN") 167 | .current_dir(project_path.join(PROJECT_NAME)) 168 | .stdout(Stdio::null()) 169 | .stderr(Stdio::null()) 170 | .output()?; 171 | if !output.status.success() { 172 | bail!("Failed to execute cargo fmt subcommand") 173 | } 174 | } 175 | 176 | Ok(()) 177 | } 178 | 179 | fn enable_config_and_dependencies(config: &mut ActiveConfiguration, option: &str) -> Result<()> { 180 | if config.selected.contains(&option.to_string()) { 181 | return Ok(()); 182 | } 183 | 184 | let option = find_option(option, &config.options) 185 | .ok_or_else(|| anyhow::anyhow!("Option not found: {option}"))?; 186 | 187 | for dependency in option.requires.iter() { 188 | if dependency.starts_with('!') { 189 | continue; 190 | } 191 | enable_config_and_dependencies(config, dependency)?; 192 | } 193 | 194 | if !config.is_option_active(option) { 195 | return Ok(()); 196 | } 197 | 198 | config.select(option.name.to_string()); 199 | 200 | Ok(()) 201 | } 202 | 203 | fn is_valid(config: &ActiveConfiguration) -> bool { 204 | let mut groups = HashSet::new(); 205 | 206 | for item in config.selected.iter() { 207 | let option = find_option(item, &config.options).unwrap(); 208 | 209 | // Option could not have been selected on UI. 210 | if !config.is_option_active(option) { 211 | return false; 212 | } 213 | 214 | // Reject combination if a selection group contains two selected options. This prevents 215 | // testing mutually exclusive options like defmt and log. 216 | if !option.selection_group.is_empty() && !groups.insert(option.selection_group.clone()) { 217 | return false; 218 | } 219 | } 220 | 221 | true 222 | } 223 | 224 | fn options_for_chip(chip: Chip, all_combinations: bool) -> Result>> { 225 | let options = include_str!("../../template/template.yaml"); 226 | let template = serde_yaml::from_str::