├── .gitignore ├── src ├── pio │ └── resources │ │ ├── main_espidf.c.resource │ │ ├── main.c.resource │ │ ├── main_arduino.cpp.resource │ │ ├── main_arduino_rust.cpp.resource │ │ ├── lib_espidf.rs.resource │ │ ├── lib.rs.resource │ │ ├── lib_arduino.rs.resource │ │ ├── dummy.c.resource │ │ ├── platformio.dump.py.resource │ │ ├── platformio.git.py.resource │ │ ├── platformio.patch.py.resource │ │ └── platformio.cargo.py.resource ├── cli.rs ├── lib.rs ├── python.rs ├── cmake │ ├── file_api.rs │ └── file_api │ │ ├── cache.rs │ │ ├── toolchains.rs │ │ ├── index.rs │ │ └── codemodel.rs ├── bingen.rs ├── utils.rs ├── fs.rs ├── espidf │ ├── resources │ │ └── cmake.json │ └── ulp_fsm.rs ├── kconfig.rs ├── cmake.rs ├── cli │ ├── separate_args.rs │ ├── parse_args.rs │ └── arg.rs ├── symgen.rs ├── cmd.rs └── bindgen.rs ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── bug_report.md ├── workflows │ ├── publish-dry-run.yml │ ├── publish-cargo-pio-dry-run.yml │ ├── publish-ldproxy-dry-run copy.yml │ ├── ci.yml │ ├── publish-ldproxy.yml │ ├── publish-cargo-pio.yml │ ├── publish.yml │ ├── release.yml │ ├── package.yml │ └── arm_linux_package.yml └── PULL_REQUEST_TEMPLATE.md ├── ldproxy ├── Cargo.toml ├── README.md ├── LICENSE-MIT ├── src │ └── main.rs └── LICENSE-APACHE ├── cargo-pio ├── Cargo.toml ├── LICENSE-MIT ├── README.md ├── src │ └── patches │ │ └── filter_exception_decoder_esp32c3_external_conf_fix.diff └── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── CHANGELOG.md ├── Cargo.toml └── LICENSE-APACHE /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /target 3 | **/*.rs.bk 4 | -------------------------------------------------------------------------------- /src/pio/resources/main_espidf.c.resource: -------------------------------------------------------------------------------- 1 | // 2 | // This function is just a sample entry point so that there are no linkage errors 3 | // 4 | 5 | void app_main() { 6 | } 7 | -------------------------------------------------------------------------------- /src/pio/resources/main.c.resource: -------------------------------------------------------------------------------- 1 | // 2 | // This function is just a sample entry point so that there are no linkage errors 3 | // 4 | 5 | int main() { 6 | return 0; 7 | } 8 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | //! CLI argument manipulation utilities. 2 | 3 | mod arg; 4 | mod parse_args; 5 | mod separate_args; 6 | 7 | pub use arg::*; 8 | pub use parse_args::*; 9 | pub use separate_args::*; 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Ask questions in Matrix channel 4 | url: https://matrix.to/#/#esp-rs:matrix.org 5 | about: Ask any questions directly in our Matrix channel. 6 | -------------------------------------------------------------------------------- /src/pio/resources/main_arduino.cpp.resource: -------------------------------------------------------------------------------- 1 | // 2 | // These functions are just sample entry points so that there are no linkage errors 3 | // 4 | 5 | #include 6 | 7 | void setup() { 8 | } 9 | 10 | void loop() { 11 | } 12 | -------------------------------------------------------------------------------- /src/pio/resources/main_arduino_rust.cpp.resource: -------------------------------------------------------------------------------- 1 | // 2 | // Arduino main stub file. Calls into Rust. 3 | // 4 | 5 | #include 6 | 7 | extern "C" void arduino_setup(); 8 | extern "C" void arduino_loop(); 9 | 10 | void setup() { 11 | arduino_setup(); 12 | } 13 | 14 | void loop() { 15 | arduino_loop(); 16 | } 17 | -------------------------------------------------------------------------------- /src/pio/resources/lib_espidf.rs.resource: -------------------------------------------------------------------------------- 1 | // Remove if STD is supported for your platform and you plan to use it 2 | #![no_std] 3 | 4 | // Remove if STD is supported for your platform and you plan to use it 5 | #[panic_handler] 6 | fn panic(_info: &core::panic::PanicInfo) -> ! { 7 | loop {} 8 | } 9 | 10 | // 11 | // Entry point 12 | // 13 | 14 | #[no_mangle] 15 | extern "C" fn app_main() { 16 | } 17 | -------------------------------------------------------------------------------- /src/pio/resources/lib.rs.resource: -------------------------------------------------------------------------------- 1 | // Remove if STD is supported for your platform and you plan to use it 2 | #![no_std] 3 | 4 | // Remove if STD is supported for your platform and you plan to use it 5 | #[panic_handler] 6 | fn panic(_info: &core::panic::PanicInfo) -> ! { 7 | loop {} 8 | } 9 | 10 | // 11 | // Entry point 12 | // 13 | 14 | #[no_mangle] 15 | extern "C" fn main() -> i32 { 16 | 0 17 | } 18 | -------------------------------------------------------------------------------- /src/pio/resources/lib_arduino.rs.resource: -------------------------------------------------------------------------------- 1 | // Remove if STD is supported for your platform and you plan to use it 2 | #![no_std] 3 | 4 | // Remove if STD is supported for your platform and you plan to use it 5 | #[panic_handler] 6 | fn panic(_info: &core::panic::PanicInfo) -> ! { 7 | loop {} 8 | } 9 | 10 | // 11 | // Entry points 12 | // 13 | 14 | #[no_mangle] 15 | extern "C" fn arduino_setup() { 16 | } 17 | 18 | #[no_mangle] 19 | extern "C" fn arduino_loop() { 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/publish-dry-run.yml: -------------------------------------------------------------------------------- 1 | name: PublishDryRun 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | publishdryrun: 7 | name: Publish Dry Run 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Setup | Checkout 11 | uses: actions/checkout@v3 12 | - name: Setup | Rust 13 | uses: dtolnay/rust-toolchain@v1 14 | with: 15 | toolchain: nightly 16 | components: rust-src 17 | - name: Build | Publish Dry Run 18 | run: cargo publish --dry-run 19 | -------------------------------------------------------------------------------- /.github/workflows/publish-cargo-pio-dry-run.yml: -------------------------------------------------------------------------------- 1 | name: PublishCargoPioDryRun 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | publishdryrun: 7 | name: Publish Dry Run 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Setup | Checkout 11 | uses: actions/checkout@v3 12 | - name: Setup | Rust 13 | uses: dtolnay/rust-toolchain@v1 14 | with: 15 | toolchain: nightly 16 | components: rust-src 17 | - name: Build | Publish Dry Run 18 | run: cd cargo-pio; cargo publish --dry-run 19 | -------------------------------------------------------------------------------- /.github/workflows/publish-ldproxy-dry-run copy.yml: -------------------------------------------------------------------------------- 1 | name: PublishLdProxyDryRun 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | publishdryrun: 7 | name: Publish Dry Run 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Setup | Checkout 11 | uses: actions/checkout@v3 12 | - name: Setup | Rust 13 | uses: dtolnay/rust-toolchain@v1 14 | with: 15 | toolchain: nightly 16 | components: rust-src 17 | - name: Build | Publish Dry Run 18 | run: cd ldproxy; cargo publish --dry-run 19 | -------------------------------------------------------------------------------- /ldproxy/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ldproxy" 3 | version = "0.3.4" 4 | edition = "2018" 5 | authors = ["Ivan Markov ", "Dominik Gschwind "] 6 | categories = ["embedded", "command-line-utilities"] 7 | keywords = ["linker", "linker-proxy", "cli"] 8 | description = "A linker proxy tool" 9 | repository = "https://github.com/ivmarkov/embuild" 10 | license = "MIT OR Apache-2.0" 11 | readme = "README.md" 12 | 13 | [dependencies] 14 | embuild = { version = "0.33", path = ".." } 15 | anyhow = {version = "1", features = ["backtrace"]} 16 | log = "0.4" 17 | env_logger = "0.9" -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: ["enhancement", "status:needs-attention"] 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Motivations 11 | 12 | 13 | 14 | - Would you like to implement this feature? [y/n] 15 | 16 | ## Solution 17 | 18 | 19 | 20 | ## Alternatives 21 | 22 | 23 | 24 | ## Additional context 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/pio/resources/dummy.c.resource: -------------------------------------------------------------------------------- 1 | // 2 | // Cargo <-> PlatformIO helper C file (autogenerated by cargo-pio) 3 | // This file is intentionally empty. Please DO NOT change it or delete it! 4 | // 5 | // Two reasons why this file is necessary: 6 | // - PlatformIO complains if the src directory is empty with an error message 7 | // 'Nothing to build. Please put your source code files to '../src' folder'. So we have to provide at least one C/C++ source file 8 | // - The Cargo invocation is attached as a post-action to building this file. This is necessary, or else 9 | // Cargo crates will not see the extra include directories of all libraries downloaded via the PlatformIO Library Manager 10 | // 11 | -------------------------------------------------------------------------------- /cargo-pio/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-pio" 3 | version = "0.26.0" 4 | edition = "2021" 5 | rust-version = "1.58" 6 | authors = ["Ivan Markov ", "Dominik Gschwind "] 7 | categories = ["embedded", "development-tools::cargo-plugins"] 8 | keywords = ["cargo", "platformio"] 9 | description = "Cargo<->PlatformIO integration: a cargo subcommand" 10 | repository = "https://github.com/ivmarkov/embuild" 11 | license = "MIT OR Apache-2.0" 12 | readme = "README.md" 13 | 14 | [dependencies] 15 | embuild = { version = "0.33", path = "..", features = ["pio"] } 16 | anyhow = {version = "1", features = ["backtrace"]} 17 | log = "0.4" 18 | env_logger = "0.9" 19 | structopt = { version = "0.3.22" } 20 | tempfile = "3.2" 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | schedule: 9 | - cron: '50 5 * * *' 10 | workflow_dispatch: 11 | 12 | jobs: 13 | compile: 14 | name: Compile 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Setup | Checkout 18 | uses: actions/checkout@v3 19 | - name: Setup | Rust 20 | uses: dtolnay/rust-toolchain@v1 21 | with: 22 | toolchain: stable 23 | components: rustfmt, clippy, rust-src 24 | - name: Build | Fmt Check 25 | run: cargo fmt -- --check 26 | - name: Build | Clippy 27 | run: cargo clippy --no-deps --workspace --all-features -- -Dwarnings 28 | - name: Build | Compile 29 | run: cargo build --workspace --all-features 30 | -------------------------------------------------------------------------------- /ldproxy/README.md: -------------------------------------------------------------------------------- 1 | # A linker proxy 2 | 3 | A simple tool to forward linker arguments to the actual linker executable also given as an argument to `ldproxy`. 4 | 5 | *Currently only gcc [linker 6 | flavor](https://doc.rust-lang.org/rustc/codegen-options/index.html#linker-flavor) is 7 | supported.* 8 | 9 | ## Special arguments 10 | 11 | These arguments are only used by `ldproxy` and not forwarded to the proxied linker. 12 | 13 | - `--ldproxy-linker=`, `--ldproxy-linker ` 14 | 15 | **required** 16 | 17 | Tells `ldproxy` the path to the linker. If multiple `--ldproxy-linker` arguments are found 18 | only the last will be used. 19 | 20 | - `--ldproxy-cwd=`, `--ldproxy-cwd ` 21 | 22 | **optional** 23 | 24 | Tells `ldproxy` the current working directory to use when it invokes the linker. 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: ["bug", "status:needs-attention"] 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Bug description 11 | 12 | 13 | 14 | - Would you like to work on a fix? [y/n] 15 | 16 | ## To Reproduce 17 | 18 | 19 | 1. ... 20 | 2. ... 21 | 22 | 23 | 24 | 25 | 26 | ## Expected behavior 27 | 28 | 29 | 30 | ## Environment 31 | 32 | - Crate (`embuild`) version: [e.g. 0.32.0; type master if you use the crate from the GIT master branch] 33 | - OS: [e.g. Ubuntu 20.04] 34 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Thank you for your contribution! 2 | 3 | We appreciate the time and effort you've put into this pull request. 4 | To help us review it efficiently, please ensure you've gone through the following checklist: 5 | 6 | ### Submission Checklist 📝 7 | - [ ] I have updated existing examples or added new ones (if applicable). 8 | - [ ] I have used `cargo fmt` command to ensure that all changed code is formatted correctly. 9 | - [ ] I have used `cargo clippy` command to ensure that all changed code passes latest Clippy nightly lints. 10 | - [ ] My changes were added to the [`CHANGELOG.md`](https://github.com/esp-rs/embuild/blob/main/embuild/CHANGELOG.md) in the **_proper_** section. 11 | 12 | ### Pull Request Details 📖 13 | 14 | #### Description 15 | Please provide a clear and concise description of your changes, including the motivation behind these changes. The context is crucial for the reviewers. 16 | 17 | #### Testing 18 | Describe how you tested your changes. -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright 2019-2020 Contributors to xtensa-lx6-rt 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /ldproxy/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright 2019-2020 Contributors to xtensa-lx6-rt 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /cargo-pio/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright 2019-2020 Contributors to xtensa-lx6-rt 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /.github/workflows/publish-ldproxy.yml: -------------------------------------------------------------------------------- 1 | name: PublishLdProxy 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | publish: 7 | name: Publish 8 | runs-on: ubuntu-latest 9 | env: 10 | CRATE_NAME: ldproxy 11 | steps: 12 | - name: Setup | Checkout 13 | uses: actions/checkout@v3 14 | - name: Setup | Rust 15 | uses: dtolnay/rust-toolchain@v1 16 | with: 17 | toolchain: nightly 18 | components: rust-src 19 | - name: Login 20 | run: cargo login ${{ secrets.crates_io_token }} 21 | - name: Build | Publish 22 | run: cd ${{env.CRATE_NAME}}; cargo publish 23 | - name: Get the crate version from cargo 24 | run: | 25 | version=$(cargo metadata --format-version=1 --no-deps | jq -r ".packages[] | select(.name == \"${{env.CRATE_NAME}}\") | .version") 26 | echo "crate_version=$version" >> $GITHUB_ENV 27 | echo "${{env.CRATE_NAME}} version: $version" 28 | - name: Tag the new release 29 | uses: rickstaa/action-create-tag@v1 30 | with: 31 | tag: ${{env.CRATE_NAME}}-v${{env.crate_version}} 32 | message: "Release ${{env.CRATE_NAME}} v${{env.crate_version}}" 33 | -------------------------------------------------------------------------------- /.github/workflows/publish-cargo-pio.yml: -------------------------------------------------------------------------------- 1 | name: PublishCargoPio 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | publish: 7 | name: Publish 8 | runs-on: ubuntu-latest 9 | env: 10 | CRATE_NAME: cargo-pio 11 | steps: 12 | - name: Setup | Checkout 13 | uses: actions/checkout@v3 14 | - name: Setup | Rust 15 | uses: dtolnay/rust-toolchain@v1 16 | with: 17 | toolchain: nightly 18 | component: rust-src 19 | - name: Login 20 | run: cargo login ${{ secrets.crates_io_token }} 21 | - name: Build | Publish 22 | run: cd ${{env.CRATE_NAME}}; cargo publish 23 | - name: Get the crate version from cargo 24 | run: | 25 | version=$(cargo metadata --format-version=1 --no-deps | jq -r ".packages[] | select(.name == \"${{env.CRATE_NAME}}\") | .version") 26 | echo "crate_version=$version" >> $GITHUB_ENV 27 | echo "${{env.CRATE_NAME}} version: $version" 28 | - name: Tag the new release 29 | uses: rickstaa/action-create-tag@v1 30 | with: 31 | tag: ${{env.CRATE_NAME}}-v${{env.crate_version}} 32 | message: "Release ${{env.CRATE_NAME}} v${{env.crate_version}}" 33 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | publish: 7 | name: Publish 8 | runs-on: ubuntu-latest 9 | env: 10 | CRATE_NAME: embuild 11 | steps: 12 | - name: Setup | Checkout 13 | uses: actions/checkout@v3 14 | - name: Setup | Rust 15 | uses: dtolnay/rust-toolchain@v1 16 | with: 17 | toolchain: nightly 18 | components: rust-src 19 | - name: Setup | Default to nightly 20 | run: rustup default nightly 21 | - name: Login 22 | run: cargo login ${{ secrets.crates_io_token }} 23 | - name: Build | Publish 24 | run: cargo publish 25 | - name: Get the crate version from cargo 26 | run: | 27 | version=$(cargo metadata --format-version=1 --no-deps | jq -r ".packages[] | select(.name == \"${{env.CRATE_NAME}}\") | .version") 28 | echo "crate_version=$version" >> $GITHUB_ENV 29 | echo "${{env.CRATE_NAME}} version: $version" 30 | - name: Tag the new release 31 | uses: rickstaa/action-create-tag@v1 32 | with: 33 | tag: v${{env.crate_version}} 34 | message: "Release v${{env.crate_version}}" 35 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Build support for embedded Rust 2 | //! 3 | //! A library with many utilities for building embedded frameworks, libraries, and other 4 | //! artifacts in a cargo build script. 5 | //! 6 | //! It is currently mainly used to simplify building the [`esp-idf`](https://github.com/espressif/esp-idf) in the build script of the 7 | //! [`esp-idf-sys`](https://github.com/esp-rs/esp-idf-sys) crate, but anyone may use them as they're intended to be general. The 8 | //! utilities are organized into specific modules so that they and their dependencies can be 9 | //! turned on or off with features. 10 | 11 | // Allows docs.rs to document any needed features for items (needs nightly rust). 12 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 13 | 14 | #[cfg(feature = "bindgen")] 15 | pub mod bindgen; 16 | 17 | #[cfg(feature = "pio")] 18 | pub mod pio; 19 | 20 | #[cfg(feature = "cmake")] 21 | pub mod cmake; 22 | 23 | #[cfg(feature = "espidf")] 24 | pub mod espidf; 25 | 26 | #[cfg(feature = "git")] 27 | pub mod git; 28 | 29 | #[cfg(feature = "kconfig")] 30 | pub mod kconfig; 31 | 32 | #[cfg(feature = "elf")] 33 | pub mod symgen; 34 | 35 | #[cfg(feature = "elf")] 36 | pub mod bingen; 37 | 38 | pub mod build; 39 | pub mod cargo; 40 | pub mod cli; 41 | pub mod cmd; 42 | pub mod fs; 43 | pub mod python; 44 | pub mod utils; 45 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | # Linux 9 | 10 | aarch64-unknown-linux-gnu: 11 | uses: ./.github/workflows/arm_linux_package.yml 12 | with: 13 | runs_on: ubuntu-18.04 14 | target: aarch64-unknown-linux-gnu 15 | 16 | x86_64-unknown-linux-gnu: 17 | uses: ./.github/workflows/package.yml 18 | with: 19 | runs_on: ubuntu-latest 20 | target: x86_64-unknown-linux-gnu 21 | 22 | x86_64-unknown-linux-musl: 23 | uses: ./.github/workflows/package.yml 24 | with: 25 | runs_on: ubuntu-latest 26 | target: x86_64-unknown-linux-musl 27 | 28 | # macOS 29 | 30 | aarch64-apple-darwin: 31 | uses: ./.github/workflows/package.yml 32 | with: 33 | runs_on: macos-latest 34 | target: aarch64-apple-darwin 35 | 36 | x86_64-apple-darwin: 37 | uses: ./.github/workflows/package.yml 38 | with: 39 | runs_on: macos-latest 40 | target: x86_64-apple-darwin 41 | 42 | # Windows 43 | 44 | x86_64-pc-windows-gnu: 45 | uses: ./.github/workflows/package.yml 46 | with: 47 | runs_on: windows-latest 48 | target: x86_64-pc-windows-gnu 49 | extension: .exe 50 | 51 | x86_64-pc-windows-msvc: 52 | uses: ./.github/workflows/package.yml 53 | with: 54 | runs_on: windows-latest 55 | target: x86_64-pc-windows-msvc 56 | extension: .exe 57 | -------------------------------------------------------------------------------- /.github/workflows/package.yml: -------------------------------------------------------------------------------- 1 | name: Package 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | runs_on: 7 | required: true 8 | type: string 9 | target: 10 | required: true 11 | type: string 12 | extension: 13 | default: "" 14 | type: string 15 | 16 | jobs: 17 | build: 18 | name: Build static binaries 19 | runs-on: ${{ inputs.runs_on }} 20 | 21 | steps: 22 | - name: Change apt mirror and install dependencies 23 | if: ${{ inputs.runs_on == 'ubuntu-latest' }} 24 | run: | 25 | sudo sed -i 's/azure.archive.ubuntu.com/archive.ubuntu.com/' /etc/apt/sources.list 26 | sudo apt-get update 27 | sudo apt-get install musl-tools libudev-dev 28 | 29 | - uses: actions/checkout@v3 30 | 31 | - uses: dtolnay/rust-toolchain@v1 32 | with: 33 | toolchain: stable 34 | target: ${{ inputs.target }} 35 | 36 | - uses: Swatinem/rust-cache@v1 37 | 38 | - run: cargo build --release --all --target ${{ inputs.target }} 39 | 40 | - uses: papeloto/action-zip@v1 41 | with: 42 | files: target/${{ inputs.target }}/release/ldproxy${{ inputs.extension }} 43 | recursive: true 44 | dest: ldproxy-${{ inputs.target }}.zip 45 | 46 | - uses: svenstaro/upload-release-action@v2 47 | with: 48 | repo_token: ${{ secrets.GITHUB_TOKEN }} 49 | file: ldproxy-${{ inputs.target }}.zip 50 | tag: ${{ github.ref }} 51 | -------------------------------------------------------------------------------- /src/pio/resources/platformio.dump.py.resource: -------------------------------------------------------------------------------- 1 | # Internal Cargo <-> PlatformIO integration script (autogenerated by cargo-pio) 2 | 3 | import os 4 | import sys 5 | import json 6 | 7 | Import("projenv") 8 | global_env = DefaultEnvironment() 9 | 10 | def action_dump(source, target, env): 11 | board_mcu = env.get("BOARD_MCU") 12 | if not board_mcu and "BOARD" in env: 13 | board_mcu = env.BoardConfig().get("build.mcu") 14 | 15 | data = { 16 | "project_dir": env.subst("$PROJECT_DIR"), 17 | "release_build": env.GetProjectOption("build_type", default = "release") == "release", 18 | 19 | "path": env["ENV"]["PATH"], 20 | "incflags": env.subst("$_CPPINCFLAGS"), 21 | "libflags": env.subst("$_LIBFLAGS"), 22 | "libdirflags": env.subst("$_LIBDIRFLAGS"), 23 | "libs": env.subst("$LIBS"), 24 | "linkflags": env.subst("$LINKFLAGS"), 25 | "link": env.subst("$LINK"), 26 | "linkcom": env.subst("$LINKCOM"), 27 | "mcu": board_mcu, 28 | 29 | "pio_platform_dir": env.PioPlatform().get_dir()[0], 30 | "pio_framework_dir": env.PioPlatform().get_package_dir("framework-" + env.GetProjectOption("framework")[0]) 31 | } 32 | 33 | with open(os.path.join(env.subst("$PROJECT_DIR"), "__pio_scons_dump.json"), "w") as file: 34 | json.dump(data, file) 35 | 36 | if projenv.GetProjectOption("quick_dump", default = "false").lower() == "true": 37 | action_dump(None, None, projenv) 38 | else: 39 | global_env.AddPreAction(os.path.join("$BUILD_DIR", "$PROGNAME$PROGSUFFIX"), action_dump) -------------------------------------------------------------------------------- /src/pio/resources/platformio.git.py.resource: -------------------------------------------------------------------------------- 1 | # A small PlatformIO integration script (autogenerated by cargo-pio) that provides the capability to clone 2 | # user-specified Git projects (platformio.ini git_repos = "...") before the build is triggered 3 | # 4 | # How to use: 5 | # Insert/update the following line in one of platformio.ini's environments: 6 | # extra_scripts = pre:platformio.git.py 7 | # Specify a newline-separated list of Git repos to check out: 8 | # git_repos = [dir-name1]@ \n [dir-name2]@... 9 | 10 | import os 11 | 12 | Import("env") 13 | 14 | class GitRepos: 15 | def run(self, env): 16 | self.__git_repos = env.GetProjectOption("git_repos", default = "") 17 | self.__materialize_git_repos() 18 | 19 | def __materialize_git_repos(self): 20 | for (directory, repo) in self.__git_repos_list(): 21 | if not os.path.exists(env.subst(directory)): 22 | print(f"Cloning {repo} as directory {directory}") 23 | if env.Execute(f"git clone {repo} {directory}"): 24 | Exit(1) 25 | 26 | def __git_repos_list(self): 27 | git_repos_dir = os.path.join("$PROJECT_WORKSPACE_DIR", "git-repos") 28 | 29 | def get_repo(item): 30 | repo = item 31 | repo_directory = repo.split("/")[-1] 32 | if "@" in item: 33 | repo_directory, repo = item.split("@", 1) 34 | 35 | return ( 36 | os.path.join(git_repos_dir, repo_directory.strip()), 37 | repo.strip()) 38 | 39 | return [get_repo(item) for item in self.__git_repos.split("\n") if len(item.strip()) > 0] 40 | 41 | GitRepos().run(env) 42 | -------------------------------------------------------------------------------- /.github/workflows/arm_linux_package.yml: -------------------------------------------------------------------------------- 1 | name: Package 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | runs_on: 7 | required: true 8 | type: string 9 | target: 10 | required: true 11 | type: string 12 | extension: 13 | default: "" 14 | type: string 15 | 16 | jobs: 17 | build: 18 | name: Build static binaries 19 | runs-on: ${{ inputs.runs_on }} 20 | 21 | steps: 22 | - uses: briansmith/actions-checkout@v2 23 | with: 24 | persist-credentials: false 25 | 26 | - name: Install dependencies 27 | run: | 28 | wget --no-check-certificate -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - 29 | sudo add-apt-repository 'deb http://apt.llvm.org/bionic/ llvm-toolchain-bionic-14 main' 30 | sudo apt-get update 31 | sudo apt-get -yq --no-install-suggests --no-install-recommends install qemu-user gcc-aarch64-linux-gnu libc6-dev-arm64-cross clang-14 llvm-14 32 | 33 | - name: Set environment 34 | run: | 35 | export TARGET_CC=clang-14 36 | export TARGET_AR=llvm-ar-14 37 | 38 | - uses: dtolnay/rust-toolchain@v1 39 | with: 40 | toolchain: nightly 41 | target: ${{ inputs.target }} 42 | 43 | - uses: Swatinem/rust-cache@v1 44 | 45 | - run: cargo install cross 46 | 47 | - run: cross build --release --all --target ${{ inputs.target }} 48 | 49 | - uses: papeloto/action-zip@v1 50 | with: 51 | files: target/${{ inputs.target }}/release/ldproxy${{ inputs.extension }} 52 | recursive: true 53 | dest: ldproxy-${{ inputs.target }}.zip 54 | 55 | - uses: svenstaro/upload-release-action@v2 56 | with: 57 | repo_token: ${{ secrets.GITHUB_TOKEN }} 58 | file: ldproxy-${{ inputs.target }}.zip 59 | tag: ${{ github.ref }} 60 | -------------------------------------------------------------------------------- /src/python.rs: -------------------------------------------------------------------------------- 1 | //! Python utilities. 2 | 3 | use anyhow::{anyhow, Context, Result}; 4 | 5 | use crate::cmd; 6 | 7 | /// Python 3 executable name. 8 | /// 9 | /// `python` for Window, `python3` otherwise. 10 | pub const PYTHON: &str = { 11 | if cfg!(windows) { 12 | // No 'python3.exe' on Windows 13 | "python" 14 | } else { 15 | "python3" 16 | } 17 | }; 18 | 19 | /// The Version of a Python Binary 20 | pub struct PythonVersion { 21 | pub major: u32, 22 | pub minor: u32, 23 | } 24 | 25 | /// Check that python is at least `major.minor`. 26 | pub fn check_python_at_least(major: u32, minor: u32) -> Result { 27 | let version_str = cmd!(PYTHON, "--version") 28 | .stdout() 29 | .context("Failed to locate python. Is python installed and in your $PATH?")?; 30 | 31 | let base_err = || anyhow!("Unexpected output from {}", PYTHON); 32 | 33 | if !version_str.starts_with("Python ") { 34 | return Err(base_err().context("Expected a version string starting with 'Python '")); 35 | } 36 | 37 | let version_str = &version_str["Python ".len()..]; 38 | let version = version_str 39 | .split('.') 40 | .map(|s| s.parse::().ok()) 41 | .collect::>(); 42 | 43 | if version.len() < 2 || version[0].is_none() || version[1].is_none() { 44 | return Err( 45 | base_err().context("Expected a version string of type '.[.remainder]'") 46 | ); 47 | } 48 | 49 | let python_major = version[0].unwrap(); 50 | let python_minor = version[1].unwrap(); 51 | 52 | if python_major < major || python_minor < minor { 53 | Err(anyhow!( 54 | "Invalid python version '{}'; expected at least {}.{}", 55 | version_str, 56 | major, 57 | minor 58 | ) 59 | .context(format!("When running '{PYTHON} --version'"))) 60 | } else { 61 | Ok(PythonVersion { 62 | major: python_major, 63 | minor: python_minor, 64 | }) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build support for embedded Rust 2 | 3 | [![CI](https://github.com/esp-rs/embuild/actions/workflows/ci.yml/badge.svg)](https://github.com/esp-rs/embuild/actions/workflows/ci.yml) 4 | ![crates.io](https://img.shields.io/crates/v/embuild.svg) 5 | [![docs.rs](https://img.shields.io/docsrs/embuild)](https://docs.rs/embuild/latest/embuild/) 6 | [![Matrix](https://img.shields.io/matrix/esp-rs:matrix.org?label=join%20matrix&color=BEC5C9&logo=matrix)](https://matrix.to/#/#esp-rs:matrix.org) 7 | 8 | A library with many utilities for building embedded frameworks, libraries, and other 9 | artifacts in a cargo build script. 10 | 11 | It is currently mainly used to simplify building the 12 | [`esp-idf`](https://github.com/espressif/esp-idf) in the build script of the 13 | [`esp-idf-sys`](https://github.com/esp-rs/esp-idf-sys) crate, but anyone may use these 14 | utilities as they're intended to be general. They're organized into specific modules so 15 | that they and their dependencies can be turned on or off with features. 16 | 17 | A list of current features and their utilities: 18 | - `pio` 19 | - Platformio support. 20 | - `cmake` 21 | - CMake file-api support and utilities. 22 | - `glob` (used in the `build` module) 23 | - Glob utilities. 24 | - `manifest` (used in the `cargo` module) 25 | - Cargo.toml and config.toml utilities. 26 | - `espidf` 27 | - An installer to install the esp-idf framework. 28 | - `git` 29 | - Git utilities for manipulating repositories using the git CLI. 30 | - `kconfig` 31 | - kconfig file parsing. 32 | - `elf` (`bingen`, `symgen` and `espidf::ulp_fsm` modules) 33 | - Elf file manipulation. 34 | 35 | Other utilities that are not behind features include: 36 | - `cargo` 37 | - Utils for interacting with cargo through the CLI, and stdout in a build script. 38 | - `cmd` 39 | - Macros and wrappers for running commands and getting their results easier. 40 | - `cli` 41 | - Command line arguments manipulation. 42 | 43 | ## Tools 44 | 45 | This repository also provides two CLI tools: 46 | 47 | - [`cargo-pio`](cargo-pio) 48 | - [`ldproxy`](ldproxy) 49 | -------------------------------------------------------------------------------- /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.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.33.1] - 2025-07-27 9 | - Fix a bug where the cmake utilities refused to work with CMake 4 due to a broken version check 10 | 11 | ## [0.33.0] - 2025-01-02 12 | 13 | ### Deprecated 14 | 15 | ### Breaking 16 | - Module `espidf`: Support building with non-git repositories (#95) 17 | - Module `espidf`: Provide tools exported env vars; do not assume that each installed tool is an executable binary 18 | - Rename `BindgenExt::headers` to `Bindgen::path_headers` to avoid collision with the existing `bindgen::Builder::headers` method 19 | 20 | ### Added 21 | - Add support for PlatformIO platform = native (#97) 22 | - Re-export the `bindgen` crate as `embuild::bindgen::types` so that downstream crates can use it without having to add it as a dependency 23 | 24 | ### Fixed 25 | 26 | ## [0.32.0] - 2024-06-23 27 | ### Breaking 28 | * bindgen: updated to the latest bindgen version. (#75) 29 | * python: check_python_at_least() now returns a Result instead of Result<()>. (#85) 30 | ### Fixed 31 | * git: speed up submodule git cloning by utilizing jobs. (#86) 32 | * esp-idf: fix builds against idf >= v5.3 by introducing new export PATH logic. (#85) 33 | * esp-idf: use correct overrides on platforms like MacOS for export PATH, etc. (#88) 34 | 35 | ## [0.31.4] - 2023-10-27 36 | * PIO: Espressif MCUs esp32c2/c5/c6/h2 had a wrong Rust target assigned to them 37 | 38 | ## [0.31.3] - 2023-08-22 39 | * New module, `espidf::sysenv` for easier propagation and consumption of the ESP IDF build settings of `esp-idf-sys` 40 | 41 | ## [0.31.2] - 2023-05-08 42 | * Compatibility with PlatformIO 6.1 43 | 44 | ## [0.31.1] - 2023-03-20 45 | * Compatibility with MacOS ARM64 46 | * Generic notion of a GIT-based SDK (used by the `esp-idf-sys` bindings for the ESP IDF and by the `chip-sys` bindings for the Matter C++ SDK) 47 | 48 | ## [0.31.0] - 2022-12-09 49 | * Bindgen dependency was bumped up to 0.63 50 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["cargo-pio", "ldproxy"] 3 | 4 | [package] 5 | name = "embuild" 6 | version = "0.33.1" 7 | authors = [ 8 | "Ivan Markov ", 9 | "Dominik Gschwind ", 10 | ] 11 | edition = "2021" 12 | rust-version = "1.59" 13 | categories = ["embedded", "development-tools::build-utils"] 14 | keywords = ["cargo", "platformio", "build-dependencies"] 15 | description = "A build support library for embedded Rust" 16 | repository = "https://github.com/ivmarkov/embuild" 17 | license = "MIT OR Apache-2.0" 18 | readme = "README.md" 19 | 20 | [package.metadata.docs.rs] 21 | all-features = true 22 | rustc-args = ["--cfg", "docsrs"] 23 | rustdoc-args = ["--cfg", "docsrs"] 24 | 25 | [features] 26 | default = [] 27 | 28 | # Platformio support 29 | pio = [ 30 | "ureq", 31 | "bindgen", 32 | "tempfile", 33 | "which", 34 | "manifest", 35 | "serde", 36 | "serde_json", 37 | ] 38 | # cmake file-api & utilities 39 | cmake = ["dep-cmake", "tempfile", "bindgen", "serde", "serde_json", "strum"] 40 | # glob utilities 41 | glob = ["globwalk"] 42 | # Cargo.toml and config.toml utilities 43 | manifest = ["cargo_toml", "toml"] 44 | # esp-idf installer 45 | espidf = [ 46 | "tempfile", 47 | "which", 48 | "git", 49 | "serde", 50 | "serde_json", 51 | "strum", 52 | "home", 53 | "regex", 54 | ] 55 | # git utilities 56 | git = ["remove_dir_all"] 57 | # kconfig utilities 58 | kconfig = ["serde", "serde_json"] 59 | # elf manipulation 60 | elf = ["xmas-elf"] 61 | 62 | [dependencies] 63 | anyhow = "1" 64 | log = "0.4" 65 | bitflags = "1" 66 | shlex = "1" 67 | thiserror = "1" 68 | filetime = "0.2" 69 | 70 | xmas-elf = { version = "0.9", optional = true } 71 | home = { version = "0.5", optional = true } 72 | strum = { version = "0.24", features = ["derive"], optional = true } 73 | serde = { version = "1", features = ["derive"], optional = true } 74 | serde_json = { version = "1", optional = true } 75 | toml = { version = "0.7", optional = true } 76 | remove_dir_all = { version = "0.8", optional = true } 77 | cargo_toml = { version = "0.15", optional = true } 78 | which = { version = "4.1", optional = true } 79 | globwalk = { version = "0.8", optional = true } 80 | tempfile = { version = "3", optional = true } 81 | ureq = { version = "2", optional = true } 82 | bindgen = { version = "0.71.1", optional = true } 83 | dep-cmake = { package = "cmake", version = "0.1", optional = true } 84 | regex = { version = "1.5", optional = true, default-features = false, features = [ 85 | "std", 86 | ] } 87 | -------------------------------------------------------------------------------- /src/cmake/file_api.rs: -------------------------------------------------------------------------------- 1 | //! The [cmake file 2 | //! API](https://cmake.org/cmake/help/git-stage/manual/cmake-file-api.7.html) used to get 3 | //! information about the build-system and build. 4 | 5 | use std::fs; 6 | use std::path::{Path, PathBuf}; 7 | 8 | use anyhow::Result; 9 | use serde::Deserialize; 10 | 11 | use crate::path_buf; 12 | 13 | /// An object or cmake version. 14 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Default)] 15 | #[serde(rename_all = "camelCase")] 16 | pub struct Version { 17 | pub major: u32, 18 | pub minor: u32, 19 | #[serde(default)] 20 | pub patch: u32, 21 | #[serde(default)] 22 | pub suffix: String, 23 | #[serde(default)] 24 | pub is_dirty: bool, 25 | } 26 | 27 | impl std::fmt::Display for Version { 28 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 29 | write!( 30 | f, 31 | "{}.{}.{}{}{}", 32 | self.major, 33 | self.minor, 34 | self.patch, 35 | if !self.suffix.is_empty() { "-" } else { "" }, 36 | self.suffix 37 | ) 38 | } 39 | } 40 | 41 | /// The query for the cmake-file-api. 42 | #[derive(Clone, Debug)] 43 | pub struct Query<'a> { 44 | api_dir: PathBuf, 45 | client_name: String, 46 | kinds: &'a [ObjKind], 47 | } 48 | 49 | impl Query<'_> { 50 | /// Create a new query. 51 | pub fn new( 52 | cmake_build_dir: impl AsRef, 53 | client_name: impl Into, 54 | kinds: &[ObjKind], 55 | ) -> Result> { 56 | let client_name = client_name.into(); 57 | let api_dir = path_buf![cmake_build_dir, ".cmake", "api", "v1"]; 58 | 59 | let client_dir = path_buf![&api_dir, "query", format!("client-{}", &client_name)]; 60 | fs::create_dir_all(&client_dir)?; 61 | 62 | for kind in kinds { 63 | fs::File::create(client_dir.join(format!( 64 | "{}-v{}", 65 | kind.as_str(), 66 | kind.supported_version() 67 | )))?; 68 | } 69 | 70 | Ok(Query { 71 | api_dir, 72 | client_name, 73 | kinds, 74 | }) 75 | } 76 | 77 | /// Try to get all replies from this query. 78 | pub fn get_replies(&self) -> Result { 79 | Replies::from_query(self) 80 | } 81 | } 82 | 83 | pub mod cache; 84 | pub mod codemodel; 85 | mod index; 86 | pub mod toolchains; 87 | 88 | pub use cache::Cache; 89 | pub use codemodel::Codemodel; 90 | pub use index::*; 91 | pub use toolchains::Toolchains; 92 | -------------------------------------------------------------------------------- /src/cmake/file_api/cache.rs: -------------------------------------------------------------------------------- 1 | //! Cache cmake file API object. 2 | 3 | use std::convert::TryFrom; 4 | use std::fs; 5 | 6 | use anyhow::{anyhow, Context, Error}; 7 | use serde::Deserialize; 8 | 9 | use super::{index, ObjKind, Version}; 10 | 11 | /// The variables stored in the persistent cache (`CMakeCache.txt`) for the build tree. 12 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] 13 | pub struct Cache { 14 | /// The version of this object kind. 15 | pub version: Version, 16 | /// All cache entries. 17 | pub entries: Vec, 18 | } 19 | 20 | impl TryFrom<&index::Reply> for Cache { 21 | type Error = Error; 22 | fn try_from(value: &index::Reply) -> Result { 23 | assert!(value.kind == ObjKind::Cache); 24 | ObjKind::Cache 25 | .check_version_supported(value.version.major) 26 | .unwrap(); 27 | 28 | serde_json::from_reader(&fs::File::open(&value.json_file)?).with_context(|| { 29 | anyhow!( 30 | "Parsing cmake-file-api cache object file '{}' failed", 31 | value.json_file.display() 32 | ) 33 | }) 34 | } 35 | } 36 | 37 | /// A cmake cache (`CMakeCache.txt`) entry. 38 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] 39 | pub struct Entry { 40 | /// The name of the entry. 41 | pub name: String, 42 | /// The value of the entry. 43 | pub value: String, 44 | /// The type of the entry. 45 | #[serde(rename = "type")] 46 | pub entry_type: Type, 47 | /// Properties set for this entries. 48 | pub properties: Vec, 49 | } 50 | 51 | /// The type of entry. 52 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] 53 | #[serde(from = "String")] 54 | pub enum Type { 55 | Bool, 56 | Path, 57 | Filepath, 58 | String, 59 | Internal, 60 | Static, 61 | Uninitialized, 62 | Other(String), 63 | } 64 | 65 | impl From for Type { 66 | fn from(s: String) -> Self { 67 | match s.as_str() { 68 | "BOOL" => Self::Bool, 69 | "PATH" => Self::Path, 70 | "FILEPATH" => Self::Filepath, 71 | "STRING" => Self::String, 72 | "INTERNAL" => Self::Internal, 73 | "STATIC" => Self::Static, 74 | "UNINITIALIZED" => Self::Uninitialized, 75 | _ => Self::Other(s), 76 | } 77 | } 78 | } 79 | 80 | /// A property set for an [`Entry`]. 81 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] 82 | #[serde(rename_all = "UPPERCASE", tag = "name", content = "value")] 83 | pub enum Property { 84 | Advanced(String), 85 | Helpstring(String), 86 | Modified(String), 87 | Strings(String), 88 | Type(Type), 89 | Value(String), 90 | #[serde(other)] 91 | Unknown, 92 | } 93 | -------------------------------------------------------------------------------- /cargo-pio/README.md: -------------------------------------------------------------------------------- 1 | # cargo-pio = [Cargo](https://doc.rust-lang.org/cargo/) + [PlatformIO](https://platformio.org/) 2 | 3 | **Build Rust embedded projects with PlatformIO!** 4 | 5 | cargo-pio is a Cargo subcommand `cargo pio`, as well as a library crate. 6 | 7 | ## Why? 8 | 9 | If you are building a mixed Rust/C project, or a pure Rust project that needs to call into the Vendor SDKs for your board, cargo-pio might help you. 10 | 11 | ## PlatformIO-first build 12 | 13 | In this mode of operation, your embedded project would be a PlatformIO project **and** a Rust static library crate: 14 | * The project build is triggered with PlatformIO (e.g. `pio run -t debug` or `cargo pio build`), and PlatformIO calls into Cargo to build the Rust library crate; 15 | * `cargo-pio` is used as a Cargo subcommand to create the layout of the project; 16 | * Such projects are called 'PIO->Cargo projects' in the `cargo-pio` help 17 | 18 | Example: 19 | * ```cargo install cargo-pio``` (or ```cargo install --git https://github.com/ivmarkov/cargo-pio.git cargo-pio```) 20 | * ```cargo pio installpio``` 21 | * Create a new Cargo/PIO project: 22 | * ```cargo pio new --board ``` 23 | * Enter your newly-generated project: 24 | * ```cd ``` 25 | * Build in debug: 26 | * ```pio run -t debug``` or ```cargo pio build``` 27 | * Build in release: 28 | * ```pio run -t release``` or ```cargo pio build --release``` 29 | * Note that once PlatformIO is installed and the PIO->Cargo project is created, you don't really need `cargo-pio`! 30 | 31 | Call ```cargo pio --help``` to learn more about the various commands supported by `cargo-pio`. 32 | 33 | ## Cargo-first build 34 | 35 | * In this mode of operation, your embedded project is a **pure Cargo project and PlatformIO does not get in the way**! 36 | * `cargo-pio` is used as a library crate and driven programmatically from `build.rs` scripts. 37 | 38 | Cargo-first builds however are less flexible. They are only suitable for building and (linking with) the "SYS" crate that represents the Vendor SDK for your board. 39 | If you depend on other C libraries, you should be using a PlatformIO-first a.k.a. 'PIO->Cargo' project. 40 | 41 | Example: 42 | * Check the [esp-idf-sys](https://crates.io/crates/esp-idf-sys) SYS crate (used by the [rust-esp32-std-hello](https://github.com/ivmarkov/rust-esp32-std-hello) binary crate). It demonstrates: 43 | * Automatic download and installation the SDK (ESP-IDF in that case), by programmatically driving PlatformIO; 44 | * Automatic generation of unsafe bindings with Bindgen for the ESP-IDF SDK. Crates like [esp-idf-hal](https://crates.io/crates/esp-idf-hal) and [esp-idf-svc](https://crates.io/crates/esp-idf-svc) depend on these bindings to implement higher level type-safe Rust abstractions; 45 | * Automatic generation of Rust link flags for the Rust Linker so that the ESP-IDF SDK is transparently linked into your binary Rust crate that you'll flash. 46 | -------------------------------------------------------------------------------- /src/cmake/file_api/toolchains.rs: -------------------------------------------------------------------------------- 1 | //! Toolchains cmake file API object. 2 | 3 | use std::convert::TryFrom; 4 | use std::fs; 5 | use std::path::PathBuf; 6 | 7 | use anyhow::{anyhow, Context, Error}; 8 | use serde::Deserialize; 9 | 10 | use super::codemodel::Language; 11 | use super::{index, ObjKind, Version}; 12 | 13 | /// Toolchain object. 14 | /// 15 | /// The toolchains object kind lists properties of the toolchains used during the build. 16 | /// These include the language, compiler path, ID, and version. 17 | #[derive(Debug, Clone, Deserialize)] 18 | pub struct Toolchains { 19 | /// Version of the object kind. 20 | pub version: Version, 21 | /// A list of toolchains associated with a particular language. 22 | pub toolchains: Vec, 23 | } 24 | 25 | impl TryFrom<&index::Reply> for Toolchains { 26 | type Error = Error; 27 | fn try_from(value: &index::Reply) -> Result { 28 | assert!(value.kind == ObjKind::Toolchains); 29 | ObjKind::Toolchains 30 | .check_version_supported(value.version.major) 31 | .unwrap(); 32 | 33 | serde_json::from_reader(&fs::File::open(&value.json_file)?).with_context(|| { 34 | anyhow!( 35 | "Parsing cmake-file-api toolchains object file '{}' failed", 36 | value.json_file.display() 37 | ) 38 | }) 39 | } 40 | } 41 | 42 | impl Toolchains { 43 | /// Get the toolchain assosicated with language `lang`. 44 | pub fn get(&self, lang: Language) -> Option<&Toolchain> { 45 | self.toolchains.iter().find(|t| t.language == lang) 46 | } 47 | 48 | /// Take the toolchain assosicated with language `lang`. 49 | pub fn take(&mut self, lang: Language) -> Option { 50 | let (i, _) = self 51 | .toolchains 52 | .iter() 53 | .enumerate() 54 | .find(|(_, t)| t.language == lang)?; 55 | Some(self.toolchains.swap_remove(i)) 56 | } 57 | } 58 | 59 | /// A toolchain associated with a particular language. 60 | #[derive(Debug, Clone, Deserialize)] 61 | pub struct Toolchain { 62 | /// The associated programming language of this toolchain. 63 | pub language: Language, 64 | /// The compiler of this toolchain. 65 | pub compiler: Compiler, 66 | } 67 | 68 | /// The compiler of a toolchain and language. 69 | #[derive(Debug, Clone, Deserialize)] 70 | #[serde(rename_all = "camelCase")] 71 | pub struct Compiler { 72 | /// The path to the compiler's executable. 73 | pub path: Option, 74 | /// The ID of the compiler. 75 | pub id: Option, 76 | /// The version of the compiler. 77 | pub version: Option, 78 | /// The cross-compiling target of the compiler. 79 | pub target: Option, 80 | /// A list of file extensions (without the leading dot) for the language's 81 | /// source files (empty if not preset). 82 | #[serde(default)] 83 | pub source_file_extensions: Vec, 84 | } 85 | -------------------------------------------------------------------------------- /src/pio/resources/platformio.patch.py.resource: -------------------------------------------------------------------------------- 1 | # A small PlatformIO integration script (autogenerated by cargo-pio) that provides the capability to patch 2 | # PlatformIO packages (or the platform itself) before the build is triggered 3 | # 4 | # How to use: 5 | # Insert/update the following line in one of platformio.ini's environments: 6 | # extra_scripts = pre:platformio.patch.py 7 | # Specify a newline-separated list of patches to apply: 8 | # patches = @ \n @... 9 | # ... where , etc. are expected to be placed in a 'patches/' folder of your project 10 | # 11 | # When is equal to "__platform__", the platform itself will be patched 12 | 13 | import os 14 | import shutil 15 | 16 | Import("env") 17 | 18 | class Patch: 19 | def run(self, env): 20 | for (patch, patch_name, dir) in self.__patches_list(env): 21 | self.__patch(env, patch, patch_name, dir) 22 | 23 | def __patch(self, env, patch, patch_name, dir): 24 | patch_flag = os.path.join(dir, f"{patch_name}.applied") 25 | 26 | if not os.path.isfile(patch_flag): 27 | # In recent PlatformIO, framework_espidf is no longer a true GIT repository 28 | # (i.e. it does not contain a .git subfolder) 29 | # As a result, "git apply" does not really work 30 | # 31 | # One workaround would've been to use `patch` instead of `git apply`, 32 | # however `patch` does not seem to be available on Windows out of the box 33 | # 34 | # Therefore, instead we do a nasty hack here: we check if `dir` 35 | # contains a `.git` folder, and if not we create it using "git init" 36 | # 37 | # Once the patch is applied, we remove the `.git` folder 38 | git_dir = os.path.join(dir, ".git") 39 | git_dir_exists = True 40 | 41 | if not os.path.exists(git_dir): 42 | if env.Execute("git init", chdir = dir): 43 | env.Exit(1) 44 | git_dir_exists = False 45 | 46 | res = env.Execute(f"git apply {patch}", chdir = dir) 47 | 48 | if not git_dir_exists: 49 | shutil.rmtree(git_dir) 50 | 51 | if res: 52 | env.Exit(1) 53 | 54 | self.__touch(patch_flag) 55 | 56 | def __patches_list(self, env): 57 | patches_dir = os.path.join(env.subst("$PROJECT_DIR"), "patches") 58 | 59 | def get_patch(item): 60 | dir, patch = item.split("@", 1) 61 | 62 | package = dir.strip() 63 | if package == "__platform__": 64 | package_dir = env.PioPlatform().get_dir() 65 | else: 66 | package_dir = env.PioPlatform().get_package_dir(package) 67 | 68 | return ( 69 | os.path.join(patches_dir, patch.strip()), 70 | patch.strip(), 71 | package_dir) 72 | 73 | return [get_patch(item) for item in env.GetProjectOption("patches", default = "").split("\n") if len(item.strip()) > 0] 74 | 75 | def __touch(self, path): 76 | os.open(path, os.O_CREAT | os.O_RDWR) 77 | 78 | Patch().run(env) 79 | -------------------------------------------------------------------------------- /src/bingen.rs: -------------------------------------------------------------------------------- 1 | use std::fs::{self, File}; 2 | use std::io::Write; 3 | use std::path::{Path, PathBuf}; 4 | use std::{cmp, env}; 5 | 6 | use anyhow::{Error, Result}; 7 | use xmas_elf::ElfFile; 8 | 9 | pub const VAR_BIN_FILE: &str = "EMBUILD_GENERATED_BIN_FILE"; 10 | 11 | pub struct Bingen { 12 | elf: PathBuf, 13 | } 14 | 15 | impl Bingen { 16 | pub fn new(elf: impl Into) -> Self { 17 | Self { elf: elf.into() } 18 | } 19 | 20 | pub fn run(&self) -> Result { 21 | let output_file = PathBuf::from(env::var("OUT_DIR")?).join("binary.bin"); 22 | 23 | self.run_for_file(&output_file)?; 24 | 25 | println!("cargo:rustc-env={}={}", VAR_BIN_FILE, output_file.display()); 26 | 27 | Ok(output_file) 28 | } 29 | 30 | pub fn run_for_file(&self, output_file: impl AsRef) -> Result<()> { 31 | let output_file = output_file.as_ref(); 32 | 33 | eprintln!("Output: {output_file:?}"); 34 | 35 | self.write(&mut File::create(output_file)?) 36 | } 37 | 38 | pub fn write(&self, output: &mut impl Write) -> Result<()> { 39 | eprintln!("Input: {:?}", self.elf); 40 | 41 | let elf_data = fs::read(&self.elf)?; 42 | let elf = ElfFile::new(&elf_data).map_err(Error::msg)?; 43 | 44 | let mut sorted = segments::segments(&elf).collect::>(); 45 | sorted.sort(); 46 | 47 | let mut offset: u64 = 0; 48 | for segment in sorted { 49 | let buf = [0_u8; 4096]; 50 | while offset < segment.addr { 51 | let delta = cmp::min(buf.len() as u64, segment.addr - offset) as usize; 52 | 53 | output.write_all(&buf[0..delta])?; 54 | 55 | offset += delta as u64; 56 | } 57 | 58 | output.write_all(segment.data)?; 59 | offset += segment.data.len() as u64; 60 | } 61 | 62 | Ok(()) 63 | } 64 | } 65 | 66 | mod segments { 67 | use std::cmp::Ordering; 68 | 69 | use xmas_elf::program::{SegmentData, Type}; 70 | use xmas_elf::ElfFile; 71 | 72 | /// A segment of code from the source elf 73 | #[derive(Debug, Ord, Eq)] 74 | pub struct CodeSegment<'a> { 75 | pub addr: u64, 76 | pub size: u64, 77 | pub data: &'a [u8], 78 | } 79 | 80 | impl PartialEq for CodeSegment<'_> { 81 | fn eq(&self, other: &Self) -> bool { 82 | self.addr.eq(&other.addr) 83 | } 84 | } 85 | 86 | impl PartialOrd for CodeSegment<'_> { 87 | fn partial_cmp(&self, other: &Self) -> Option { 88 | Some(self.cmp(other)) 89 | } 90 | } 91 | 92 | pub fn segments<'a>(elf: &'a ElfFile<'a>) -> impl Iterator> + 'a { 93 | elf.program_iter() 94 | .filter(|header| { 95 | header.file_size() > 0 && header.get_type() == Ok(Type::Load) && header.offset() > 0 96 | }) 97 | .flat_map(move |header| { 98 | let addr = header.virtual_addr(); 99 | let size = header.file_size(); 100 | let data = match header.get_data(elf) { 101 | Ok(SegmentData::Undefined(data)) => data, 102 | _ => return None, 103 | }; 104 | Some(CodeSegment { addr, data, size }) 105 | }) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | //! Miscellaneous utilities. 2 | 3 | use std::ffi::OsStr; 4 | use std::path::{Path, PathBuf}; 5 | use std::{env, io}; 6 | 7 | use anyhow::Result; 8 | 9 | /// Build a [`PathBuf`]. 10 | /// 11 | /// # Examples 12 | /// 13 | /// ``` 14 | /// use std::path::Path; 15 | /// use embuild::path_buf; 16 | /// assert_eq!(path_buf!["/foo", "bar"].as_path(), Path::new("/foo/bar")); 17 | /// ``` 18 | #[macro_export] 19 | macro_rules! path_buf { 20 | ($($e: expr),*) => {{ 21 | use std::path::PathBuf; 22 | let mut pb = PathBuf::new(); 23 | $( 24 | pb.push($e); 25 | )* 26 | pb 27 | }} 28 | } 29 | 30 | /// [`Path`] extension methods. 31 | pub trait PathExt: AsRef { 32 | /// Pop `times` segments from this path. 33 | fn pop_times(&self, times: usize) -> PathBuf { 34 | let mut path: PathBuf = self.as_ref().to_owned(); 35 | for _ in 0..times { 36 | path.pop(); 37 | } 38 | path 39 | } 40 | 41 | /// Make this path absolute relative to `relative_dir` if not already. 42 | /// 43 | /// Note: Does not check if the path exists and no normalization takes place. 44 | fn abspath_relative_to(&self, relative_dir: impl AsRef) -> PathBuf { 45 | if self.as_ref().is_absolute() { 46 | return self.as_ref().to_owned(); 47 | } 48 | 49 | relative_dir.as_ref().join(self) 50 | } 51 | 52 | /// Make this path absolute relative to [`env::current_dir`] if not already. 53 | /// 54 | /// Note: Does not check if the path exists and no normalization takes place. 55 | fn abspath(&self) -> io::Result { 56 | if self.as_ref().is_absolute() { 57 | return Ok(self.as_ref().to_owned()); 58 | } 59 | 60 | Ok(env::current_dir()?.join(self)) 61 | } 62 | } 63 | 64 | impl PathExt for Path {} 65 | impl PathExt for PathBuf {} 66 | 67 | /// Error when converting from [`OsStr`] to [`String`] fails. 68 | /// 69 | /// The contained [`String`] is is the lossy conversion of the original. 70 | #[derive(Debug, thiserror::Error)] 71 | #[error("failed to convert OsStr '{0}' to String, invalid utf-8")] 72 | pub struct Utf8ConvError(pub String); 73 | 74 | /// Extension trait for fallibly converting [`OsStr`] to [`str`]. 75 | pub trait OsStrExt: AsRef { 76 | /// Try to convert this [`OsStr`] into a string. 77 | fn try_to_str(&self) -> Result<&str, Utf8ConvError> { 78 | match self.as_ref().to_str() { 79 | Some(s) => Ok(s), 80 | _ => Err(Utf8ConvError(self.as_ref().to_string_lossy().to_string())), 81 | } 82 | } 83 | } 84 | 85 | impl OsStrExt for OsStr {} 86 | impl OsStrExt for std::ffi::OsString {} 87 | impl OsStrExt for Path {} 88 | impl OsStrExt for PathBuf {} 89 | 90 | /// Download the file at `url` to `writer`. 91 | /// 92 | /// Fails if the response status is not `200` (`OK`). 93 | #[cfg(feature = "ureq")] 94 | pub fn download_file_to(url: &str, writer: &mut impl std::io::Write) -> Result<()> { 95 | let req = ureq::get(url).call()?; 96 | if req.status() != 200 { 97 | anyhow::bail!( 98 | "Server at url '{}' returned unexpected status {}: {}", 99 | url, 100 | req.status(), 101 | req.status_text() 102 | ); 103 | } 104 | 105 | let mut reader = req.into_reader(); 106 | std::io::copy(&mut reader, writer)?; 107 | Ok(()) 108 | } 109 | -------------------------------------------------------------------------------- /src/fs.rs: -------------------------------------------------------------------------------- 1 | //! Filesystem utilities. 2 | 3 | use std::fs::{self, File}; 4 | use std::io::{self, Read}; 5 | use std::path::Path; 6 | 7 | use anyhow::Result; 8 | 9 | /// Copy `src_file` to `dest_file_or_dir` if `src_file` is different or the destination 10 | /// file doesn't exist. 11 | /// 12 | /// ### Panics 13 | /// If `src_file` is not a file this function will panic. 14 | pub fn copy_file_if_different( 15 | src_file: impl AsRef, 16 | dest_file_or_dir: impl AsRef, 17 | ) -> Result<()> { 18 | let src_file: &Path = src_file.as_ref(); 19 | let dest_file_or_dir: &Path = dest_file_or_dir.as_ref(); 20 | 21 | assert!(src_file.is_file()); 22 | 23 | let src_fd = fs::File::open(src_file)?; 24 | 25 | let (dest_fd, dest_file) = if dest_file_or_dir.exists() { 26 | if dest_file_or_dir.is_dir() { 27 | let dest_file = dest_file_or_dir.join(src_file.file_name().unwrap()); 28 | if dest_file.exists() { 29 | (Some(fs::File::open(&dest_file)?), dest_file) 30 | } else { 31 | (None, dest_file) 32 | } 33 | } else { 34 | ( 35 | Some(fs::File::open(dest_file_or_dir)?), 36 | dest_file_or_dir.to_owned(), 37 | ) 38 | } 39 | } else { 40 | (None, dest_file_or_dir.to_owned()) 41 | }; 42 | 43 | if let Some(dest_fd) = dest_fd { 44 | if !is_file_eq(&src_fd, &dest_fd)? { 45 | drop(dest_fd); 46 | drop(src_fd); 47 | copy_with_metadata(src_file, dest_file)?; 48 | } 49 | } else { 50 | copy_with_metadata(src_file, dest_file)?; 51 | } 52 | Ok(()) 53 | } 54 | 55 | /// Whether the file type and contents of `file` are equal to `other`. 56 | pub fn is_file_eq(file: &File, other: &File) -> Result { 57 | let file_meta = file.metadata()?; 58 | let other_meta = other.metadata()?; 59 | 60 | if file_meta.file_type() == other_meta.file_type() 61 | && file_meta.len() == other_meta.len() 62 | && file_meta.modified()? == other_meta.modified()? 63 | { 64 | let mut file_bytes = io::BufReader::new(file).bytes(); 65 | let mut other_bytes = io::BufReader::new(other).bytes(); 66 | 67 | // TODO: check performance 68 | loop { 69 | match (file_bytes.next(), other_bytes.next()) { 70 | (Some(Ok(b0)), Some(Ok(b1))) => { 71 | if b0 != b1 { 72 | break Ok(false); 73 | } 74 | } 75 | (None, None) => break Ok(true), 76 | (None, Some(_)) | (Some(_), None) => break Ok(false), 77 | (Some(Err(e)), _) | (_, Some(Err(e))) => return Err(e.into()), 78 | } 79 | } 80 | } else { 81 | Ok(false) 82 | } 83 | } 84 | 85 | /// Wrap [`fs::copy`] to also copy metadata such as permissions and file times. 86 | /// 87 | /// This function is required because Cargo's fingerprinting uses mtime which is not perpetuated by 88 | /// [`fs::copy`]. If Cargo detects a fingerprint mismatch, it will rebuild the crate with the 89 | /// mismatch. This could lead Cargo to keep rebuilding the same crate over and over because the 90 | /// comparison will always yield a new fingerprint since [`fs::copy`] doesn't take mtime into 91 | /// account. 92 | pub fn copy_with_metadata(src_file: impl AsRef, dest_file: impl AsRef) -> Result<()> { 93 | fs::copy(&src_file, &dest_file)?; 94 | let src_file_meta = fs::File::open(&src_file)?.metadata()?; 95 | 96 | let src_atime = filetime::FileTime::from_last_access_time(&src_file_meta); 97 | let src_mtime = filetime::FileTime::from_last_modification_time(&src_file_meta); 98 | 99 | fs::set_permissions(dest_file.as_ref(), src_file_meta.permissions())?; 100 | filetime::set_file_times(dest_file, src_atime, src_mtime)?; 101 | 102 | Ok(()) 103 | } 104 | -------------------------------------------------------------------------------- /src/espidf/resources/cmake.json: -------------------------------------------------------------------------------- 1 | { 2 | "tools": [ 3 | { 4 | "description": "CMake build system", 5 | "export_paths": [ 6 | [ 7 | "bin" 8 | ] 9 | ], 10 | "export_vars": {}, 11 | "info_url": "https://github.com/Kitware/CMake", 12 | "install": "on_request", 13 | "license": "BSD-3-Clause", 14 | "name": "cmake", 15 | "platform_overrides": [ 16 | { 17 | "install": "always", 18 | "platforms": [ 19 | "win32", 20 | "win64" 21 | ] 22 | }, 23 | { 24 | "export_paths": [ 25 | [ 26 | "CMake.app", 27 | "Contents", 28 | "bin" 29 | ] 30 | ], 31 | "platforms": [ 32 | "macos", 33 | "macos-arm64" 34 | ] 35 | } 36 | ], 37 | "strip_container_dirs": 1, 38 | "supported_targets": [ 39 | "all" 40 | ], 41 | "version_cmd": [ 42 | "cmake", 43 | "--version" 44 | ], 45 | "version_regex": "cmake version ([0-9.]+)", 46 | "versions": [ 47 | { 48 | "linux-amd64": { 49 | "sha256": "97bf730372f9900b2dfb9206fccbcf92f5c7f3b502148b832e77451aa0f9e0e6", 50 | "size": 43877847, 51 | "url": "https://github.com/Kitware/CMake/releases/download/v3.20.3/cmake-3.20.3-linux-x86_64.tar.gz" 52 | }, 53 | "linux-arm64": { 54 | "sha256": "77620f99e9d5f39cf4a49294c6a68c89a978ecef144894618974b9958efe3c2a", 55 | "size": 45139836, 56 | "url": "https://github.com/Kitware/CMake/releases/download/v3.20.3/cmake-3.20.3-linux-aarch64.tar.gz" 57 | }, 58 | "linux-armel": { 59 | "sha256": "f8bd050c2745f0dcc4b7cef9738bbfef775950a10f5bd377abb0062835e669dc", 60 | "size": 13759084, 61 | "url": "https://dl.espressif.com/dl/cmake/cmake-3.20.3-Linux-armv7l.tar.gz" 62 | }, 63 | "macos": { 64 | "sha256": "5f72dba3aa5f3800fb29ab6115ae0b31f10bdb2aad66204e14c98f6ac7e6b6ed", 65 | "size": 66311879, 66 | "url": "https://github.com/Kitware/CMake/releases/download/v3.20.3/cmake-3.20.3-macos-universal.tar.gz" 67 | }, 68 | "macos-arm64": { 69 | "sha256": "5f72dba3aa5f3800fb29ab6115ae0b31f10bdb2aad66204e14c98f6ac7e6b6ed", 70 | "size": 66311879, 71 | "url": "https://github.com/Kitware/CMake/releases/download/v3.20.3/cmake-3.20.3-macos-universal.tar.gz" 72 | }, 73 | "name": "3.20.3", 74 | "status": "recommended", 75 | "win32": { 76 | "sha256": "e276cf7fbb3e3e88bc666e183bc3ddaceb143a4c83fb357b1dbb1a26fd6e4ea2", 77 | "size": 36995168, 78 | "url": "https://github.com/Kitware/CMake/releases/download/v3.20.3/cmake-3.20.3-windows-x86_64.zip" 79 | }, 80 | "win64": { 81 | "sha256": "e276cf7fbb3e3e88bc666e183bc3ddaceb143a4c83fb357b1dbb1a26fd6e4ea2", 82 | "size": 36995168, 83 | "url": "https://github.com/Kitware/CMake/releases/download/v3.20.3/cmake-3.20.3-windows-x86_64.zip" 84 | } 85 | } 86 | ] 87 | } 88 | ], 89 | "version": 1 90 | } -------------------------------------------------------------------------------- /ldproxy/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::env; 3 | use std::path::Path; 4 | use std::process::Command; 5 | use std::vec::Vec; 6 | 7 | use anyhow::{bail, Result}; 8 | use embuild::build; 9 | use embuild::cli::{ParseFrom, UnixCommandArgs}; 10 | use log::*; 11 | 12 | fn main() -> Result<()> { 13 | env_logger::Builder::from_env( 14 | env_logger::Env::new() 15 | .write_style_or("LDPROXY_LOG_STYLE", "Auto") 16 | .filter_or("LDPROXY_LOG", LevelFilter::Info.to_string()), 17 | ) 18 | .target(env_logger::Target::Stderr) 19 | .format_level(false) 20 | .format_indent(None) 21 | .format_module_path(false) 22 | .format_timestamp(None) 23 | .init(); 24 | 25 | info!("Running ldproxy"); 26 | 27 | debug!("Raw link arguments: {:?}", env::args()); 28 | 29 | let mut args = args()?; 30 | 31 | debug!("Link arguments: {args:?}"); 32 | 33 | let [linker, remove_duplicate_libs, cwd] = [ 34 | &build::LDPROXY_LINKER_ARG, 35 | &build::LDPROXY_DEDUP_LIBS_ARG, 36 | &build::LDPROXY_WORKING_DIRECTORY_ARG, 37 | ] 38 | .parse_from(&mut args); 39 | 40 | let linker = linker 41 | .ok() 42 | .and_then(|v| v.into_iter().next_back()) 43 | .unwrap_or_else(|| { 44 | panic!( 45 | "Cannot locate argument '{}'", 46 | build::LDPROXY_LINKER_ARG.format(Some("")) 47 | ) 48 | }); 49 | 50 | debug!("Actual linker executable: {linker}"); 51 | 52 | let cwd = cwd.ok().and_then(|v| v.into_iter().next_back()); 53 | let remove_duplicate_libs = remove_duplicate_libs.is_ok(); 54 | 55 | let args = if remove_duplicate_libs { 56 | debug!("Duplicate libs removal requested"); 57 | 58 | let mut libs = HashMap::::new(); 59 | 60 | for arg in &args { 61 | if arg.starts_with("-l") { 62 | *libs.entry(arg.clone()).or_default() += 1; 63 | } 64 | } 65 | 66 | debug!("Libs occurances: {libs:?}"); 67 | 68 | let mut deduped_args = Vec::new(); 69 | 70 | for arg in args { 71 | if libs.contains_key(&arg) { 72 | *libs.get_mut(&arg).unwrap() -= 1; 73 | 74 | if libs[&arg] == 0 { 75 | libs.remove(&arg); 76 | } 77 | } 78 | 79 | if !libs.contains_key(&arg) { 80 | deduped_args.push(arg); 81 | } 82 | } 83 | 84 | deduped_args 85 | } else { 86 | args 87 | }; 88 | 89 | let mut cmd = Command::new(&linker); 90 | if let Some(cwd) = cwd { 91 | cmd.current_dir(cwd); 92 | } 93 | cmd.args(&args); 94 | 95 | debug!("Calling actual linker: {cmd:?}"); 96 | 97 | let output = cmd.output()?; 98 | let stdout = String::from_utf8(output.stdout)?; 99 | let stderr = String::from_utf8(output.stderr)?; 100 | 101 | debug!("==============Linker stdout:\n{stdout}\n=============="); 102 | debug!("==============Linker stderr:\n{stderr}\n=============="); 103 | 104 | if !output.status.success() { 105 | bail!( 106 | "Linker {linker} failed: {}\nSTDERR OUTPUT:\n{stderr}", 107 | output.status 108 | ); 109 | } 110 | 111 | if env::var("LDPROXY_LINK_FAIL").is_ok() { 112 | bail!("Failure requested"); 113 | } 114 | 115 | Ok(()) 116 | } 117 | 118 | /// Get all arguments 119 | /// 120 | /// **Currently only supports gcc-like arguments** 121 | /// 122 | /// FIXME: handle other linker flavors (https://doc.rust-lang.org/rustc/codegen-options/index.html#linker-flavor) 123 | fn args() -> Result> { 124 | let mut result = Vec::new(); 125 | 126 | for arg in env::args().skip(1) { 127 | // Rustc could invoke use with response file arguments, so we could get arguments 128 | // like: `@` (as per `@file` section of 129 | // https://gcc.gnu.org/onlinedocs/gcc-11.2.0/gcc/Overall-Options.html) 130 | // 131 | // Deal with that 132 | if let Some(rsp_file_str) = arg.strip_prefix('@') { 133 | let rsp_file = Path::new(rsp_file_str); 134 | // get all arguments from the response file if it exists 135 | if rsp_file.exists() { 136 | let contents = std::fs::read_to_string(rsp_file)?; 137 | debug!("Contents of {}: {}", rsp_file_str, contents); 138 | 139 | result.extend(UnixCommandArgs::new(&contents)); 140 | } 141 | // otherwise just add the argument as normal 142 | else { 143 | result.push(arg); 144 | } 145 | } else { 146 | result.push(arg); 147 | } 148 | } 149 | 150 | Ok(result) 151 | } 152 | -------------------------------------------------------------------------------- /src/kconfig.rs: -------------------------------------------------------------------------------- 1 | //! A quick and dirty parser for the .config files generated by kconfig systems (e.g. used 2 | //! in the esp-idf). 3 | 4 | use std::collections::HashMap; 5 | use std::fs; 6 | use std::io::{self, BufRead, Read}; 7 | use std::path::Path; 8 | 9 | use anyhow::Result; 10 | 11 | /// A tristate kconfig configuration item. 12 | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 13 | pub enum Tristate { 14 | /// The item is enabled, compiled, true. 15 | True, 16 | /// The item is disabled, excluded, false. 17 | False, 18 | /// The item is compiled as module. 19 | /// 20 | /// Related to a linux kernel functionality that should be compiled as a loadable 21 | /// module (hence the name). 22 | Module, 23 | /// The item is unset. 24 | NotSet, 25 | } 26 | 27 | /// Value of a kconfig configuration item. 28 | #[derive(Clone, Debug)] 29 | pub enum Value { 30 | /// A [`Tristate`] value. 31 | Tristate(Tristate), 32 | /// A [`String`] value. 33 | String(String), 34 | } 35 | 36 | impl Value { 37 | /// Turn a configuration value of an item named `key` into a valid rust cfg. 38 | /// 39 | /// Only the following cfgs will be generated: 40 | /// - For a [`Tristate::True`], `_`; 41 | /// - for a [`String`], `_=""`. 42 | /// 43 | /// All other values return [`None`]. 44 | /// 45 | /// Both `prefix` and `key` are lowercased. 46 | pub fn to_rustc_cfg(&self, prefix: impl AsRef, key: impl AsRef) -> Option { 47 | match self { 48 | Value::Tristate(Tristate::True) => Some(""), 49 | Value::String(s) => Some(s.as_str()), 50 | _ => None, 51 | } 52 | .map(|value| { 53 | if value.is_empty() { 54 | format!( 55 | "{}_{}", 56 | prefix.as_ref().to_lowercase(), 57 | key.as_ref().to_lowercase() 58 | ) 59 | } else { 60 | format!( 61 | "{}_{}=\"{}\"", 62 | prefix.as_ref().to_lowercase(), 63 | key.as_ref().to_lowercase(), 64 | value.replace('\"', "\\\"") 65 | ) 66 | } 67 | }) 68 | } 69 | } 70 | 71 | /// Try to load the configurations from a generated kconfig json file. 72 | pub fn try_from_json_file(path: impl AsRef) -> Result> { 73 | try_from_json(fs::File::open(path)?) 74 | } 75 | 76 | /// Try to load the configurations from a generated kconfig json stream. 77 | pub fn try_from_json(reader: R) -> Result> 78 | where 79 | R: Read, 80 | { 81 | let values: HashMap = serde_json::from_reader(reader)?; 82 | 83 | let iter = values.into_iter().filter_map(|(k, v)| match v { 84 | serde_json::Value::Bool(true) => Some((k, Value::Tristate(Tristate::True))), 85 | serde_json::Value::Bool(false) => Some((k, Value::Tristate(Tristate::False))), 86 | serde_json::Value::String(value) => Some((k, Value::String(value))), 87 | _ => None, 88 | }); 89 | 90 | Ok(iter) 91 | } 92 | 93 | /// Try to load the configurations from a generated .config file. 94 | pub fn try_from_config_file( 95 | path: impl AsRef, 96 | ) -> Result> { 97 | try_from_config(fs::File::open(path.as_ref())?) 98 | } 99 | 100 | /// Try to load the configurations from a generated .config stream. 101 | pub fn try_from_config(reader: R) -> Result> 102 | where 103 | R: Read, 104 | { 105 | let iter = io::BufReader::new(reader) 106 | .lines() 107 | .filter_map(|line| line.ok().map(|l| l.trim().to_owned())) 108 | .filter(|line| !line.starts_with('#')) 109 | .filter_map(|line| { 110 | let mut split = line.split('='); 111 | 112 | if let Some(key) = split.next() { 113 | split 114 | .next() 115 | .map(|v| v.trim()) 116 | .and_then(parse_config_value) 117 | .map(|value| (key.to_owned(), value)) 118 | } else { 119 | None 120 | } 121 | }); 122 | 123 | Ok(iter) 124 | } 125 | 126 | fn parse_config_value(str: impl AsRef) -> Option { 127 | let str = str.as_ref(); 128 | 129 | Some(if str.starts_with('\"') { 130 | Value::String(str[1..str.len() - 1].to_owned()) 131 | } else if str == "y" { 132 | Value::Tristate(Tristate::True) 133 | } else if str == "n" { 134 | Value::Tristate(Tristate::False) 135 | } else if str == "m" { 136 | Value::Tristate(Tristate::Module) 137 | } else { 138 | return None; 139 | }) 140 | } 141 | -------------------------------------------------------------------------------- /src/pio/resources/platformio.cargo.py.resource: -------------------------------------------------------------------------------- 1 | # Cargo <-> PlatformIO integration script (autogenerated by cargo-pio) 2 | # Calling 'pio run' will also build the Rust library crate by invoking Cargo 3 | # 4 | # How to use: Insert/update the following line in one of platformio.ini's environments: 5 | # extra_scripts = platformio.cargo.py 6 | 7 | import os 8 | 9 | Import("env") 10 | 11 | class Cargo: 12 | def run(self, env): 13 | self.__init_props(env) 14 | 15 | if self.__cargo_run_before_project: 16 | # Attach as a pre-action to all source files so that in case CBindgen is used 17 | # the C headers are generated before the files are compiled 18 | env.AddPreAction(Glob(os.path.join(env.subst("$BUILD_DIR"), "src/*.o")), self.__run_cargo) 19 | 20 | # Hack. Need to always run when a C file from the src directory is built, or else the include directories 21 | # passed to Cargo will not contain the includes coming from libraries imported with PlatformIO's Library Manager 22 | env.AlwaysBuild(os.path.join(env.subst("$BUILD_DIR"), "src/dummy.o")) 23 | 24 | env.AddPreAction("$BUILD_DIR/$PROGNAME$PROGSUFFIX", [self.__run_cargo, self.__link_cargo]) 25 | 26 | def __init_props(self, env): 27 | self.__cargo_ran = False 28 | 29 | self.__rust_lib = env.GetProjectOption("rust_lib") 30 | self.__rust_target = env.GetProjectOption("rust_target", default = None) 31 | 32 | self.__rust_bindgen_enabled = env.GetProjectOption("rust_bindgen_enabled", default = "false").lower() == "true" 33 | self.__rust_bindgen_extra_clang_args = env.GetProjectOption("rust_bindgen_extra_clang_args", default = "") 34 | 35 | self.__cargo_run_before_project = env.GetProjectOption("cargo_run_before_project", default = "false").lower() == "true" 36 | self.__cargo_options = env.GetProjectOption("cargo_options", default = "") 37 | self.__cargo_profile = env.GetProjectOption( 38 | "cargo_profile", 39 | default = "release" if env.GetProjectOption("build_type") == "release" else "debug") 40 | self.__cargo_target_dir = env.GetProjectOption( 41 | "cargo_target_dir", 42 | default = os.path.join(env.subst("$PROJECT_BUILD_DIR"), "cargo") 43 | if env.GetProjectOption("cargo_pio_common_build_dir", default = "").lower() == "true" 44 | else os.path.join(env.subst("$PROJECT_DIR"), "target")) 45 | 46 | def __run_cargo(self, source, target, env): 47 | if self.__cargo_ran: 48 | return 0 49 | 50 | print(">>> CARGO") 51 | 52 | board_mcu = env.get("BOARD_MCU") 53 | if not board_mcu and "BOARD" in env: 54 | board_mcu = env.BoardConfig().get("build.mcu") 55 | 56 | env["ENV"]["CARGO_BUILD_TARGET_DIR"] = self.__cargo_target_dir 57 | env["ENV"]["CARGO_PIO_BUILD_PROJECT_DIR"] = env.subst("$PROJECT_DIR") 58 | env["ENV"]["CARGO_PIO_BUILD_RELEASE_BUILD"] = str(env.GetProjectOption("build_type", default = "release") == "release") 59 | 60 | env["ENV"]["CARGO_PIO_BUILD_PATH"] = env["ENV"]["PATH"] 61 | env["ENV"]["CARGO_PIO_BUILD_ACTIVE"] = "1" 62 | env["ENV"]["CARGO_PIO_BUILD_INC_FLAGS"] = env.subst("$_CPPINCFLAGS") 63 | env["ENV"]["CARGO_PIO_BUILD_LIB_FLAGS"] = env.subst("$_LIBFLAGS") 64 | env["ENV"]["CARGO_PIO_BUILD_LIB_DIR_FLAGS"] = env.subst("$_LIBDIRFLAGS") 65 | env["ENV"]["CARGO_PIO_BUILD_LIBS"] = env.subst("$LIBS") 66 | env["ENV"]["CARGO_PIO_BUILD_LINK_FLAGS"] = env.subst("$LINKFLAGS") 67 | env["ENV"]["CARGO_PIO_BUILD_LINK"] = env.subst("$LINK") 68 | env["ENV"]["CARGO_PIO_BUILD_LINKCOM"] = env.subst("$LINKCOM") 69 | if board_mcu is not None: 70 | env["ENV"]["CARGO_PIO_BUILD_MCU"] = board_mcu 71 | 72 | if self.__rust_bindgen_enabled: 73 | env["ENV"]["CARGO_PIO_BUILD_BINDGEN_RUN"] = "True" 74 | env["ENV"]["CARGO_PIO_BUILD_BINDGEN_EXTRA_CLANG_ARGS"] = self.__rust_bindgen_extra_clang_args 75 | 76 | pio_platform_dir = env.PioPlatform().get_dir()[0] 77 | if pio_platform_dir is not None: 78 | env["ENV"]["CARGO_PIO_BUILD_PIO_PLATFORM_DIR"] = pio_platform_dir 79 | framework = env.GetProjectOption("framework") 80 | if framework: 81 | pio_framework_dir = env.PioPlatform().get_package_dir(env.PioPlatform().frameworks[framework[0]]["package"]) 82 | if pio_framework_dir is not None: 83 | env["ENV"]["CARGO_PIO_BUILD_PIO_FRAMEWORK_DIR"] = pio_framework_dir 84 | if self.__rust_target is not None: 85 | cargo_target_option = f"--target {self.__rust_target}" 86 | else: 87 | cargo_target_option = "" 88 | 89 | self.__cargo_ran = True 90 | result = env.Execute(f"cargo build {'--release' if self.__cargo_profile == 'release' else ''} --lib {cargo_target_option} {self.__cargo_options}") 91 | 92 | print("<<< CARGO") 93 | 94 | return result 95 | 96 | def __link_cargo(self, source, target, env): 97 | env.Prepend(LINKFLAGS = ["-Wl,--allow-multiple-definition"]) # A hack to workaround this issue with Rust's compiler intrinsics: https://github.com/rust-lang/compiler-builtins/issues/353 98 | if self.__rust_target is not None: 99 | cargo_profile_path = os.path.join(self.__cargo_target_dir, self.__rust_target, self.__cargo_profile) 100 | else: 101 | cargo_profile_path = os.path.join(self.__cargo_target_dir, self.__cargo_profile) 102 | env.Prepend(LIBPATH = [env.subst(cargo_profile_path)]) 103 | env.Prepend(LIBS = [self.__rust_lib]) 104 | 105 | Cargo().run(env) 106 | -------------------------------------------------------------------------------- /src/cmake.rs: -------------------------------------------------------------------------------- 1 | //! CMake file API and other utilities. 2 | 3 | use std::collections::HashMap; 4 | use std::convert::TryFrom; 5 | use std::env; 6 | use std::ffi::OsString; 7 | use std::fs::File; 8 | use std::io::Write; 9 | use std::path::Path; 10 | 11 | use anyhow::{Error, Result}; 12 | use strum::{Display, EnumIter, EnumString, IntoStaticStr}; 13 | 14 | use crate::build::{CInclArgs, LinkArgsBuilder}; 15 | use crate::cli::NativeCommandArgs; 16 | use crate::cmd; 17 | 18 | pub mod file_api; 19 | pub use dep_cmake::*; 20 | pub use file_api::Query; 21 | 22 | /// An enum for parsing and passing to cmake the standard command-line generators. 23 | #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, EnumString, Display, EnumIter, IntoStaticStr)] 24 | #[strum(ascii_case_insensitive)] 25 | pub enum Generator { 26 | Ninja, 27 | NinjaMultiConfig, 28 | UnixMakefiles, 29 | BorlandMakefiles, 30 | MSYSMakefiles, 31 | MinGWMakefiles, 32 | NMakeMakefiles, 33 | NMakeMakefilesJOM, 34 | WatcomWMake, 35 | } 36 | 37 | impl Generator { 38 | pub fn name(&self) -> &'static str { 39 | match self { 40 | Self::Ninja => "Ninja", 41 | Self::NinjaMultiConfig => "Ninja MultiConfig", 42 | Self::UnixMakefiles => "Unix Makefiles", 43 | Self::BorlandMakefiles => "Borland Makefiles", 44 | Self::MSYSMakefiles => "MSYS Makefiles", 45 | Self::MinGWMakefiles => "MinGW Makefiles", 46 | Self::NMakeMakefiles => "NMake Makefiles", 47 | Self::NMakeMakefilesJOM => "NMake Makefiles JOM", 48 | Self::WatcomWMake => "Watcom WMake", 49 | } 50 | } 51 | } 52 | 53 | /// Get all variables defined in the `cmake_script_file`. 54 | /// 55 | /// #### Note 56 | /// This will run the script using `cmake -P`, beware of any side effects. Variables that 57 | /// cmake itself sets will also be returned. 58 | pub fn get_script_variables( 59 | cmake_script_file: impl AsRef, 60 | ) -> Result> { 61 | let temp_file = script_variables_extractor(cmake_script_file)?; 62 | 63 | let output = cmd!(cmake(), "-P", temp_file.as_ref()).stdout()?; 64 | drop(temp_file); 65 | 66 | let output = process_script_variables_extractor_output(output)?; 67 | 68 | Ok(output) 69 | } 70 | 71 | /// Augment a `cmake_script_file` with code that will extract all variables from the script 72 | /// when executing the augmented script. Variables that cmake itself sets will also be returned. 73 | /// 74 | /// #### Note 75 | /// Run the returned script using `cmake -P`, beware of any side effects. 76 | pub fn script_variables_extractor(cmake_script_file: impl AsRef) -> Result> { 77 | let mut temp_file = tempfile::NamedTempFile::new()?; 78 | std::io::copy(&mut File::open(cmake_script_file)?, &mut temp_file)?; 79 | 80 | temp_file.write_all( 81 | r#" 82 | message(STATUS "VARIABLE_DUMP_START") 83 | get_cmake_property(_variableNames VARIABLES) 84 | list (SORT _variableNames) 85 | foreach (_variableName ${_variableNames}) 86 | message(STATUS "${_variableName}=${${_variableName}}") 87 | endforeach() 88 | "# 89 | .as_bytes(), 90 | )?; 91 | 92 | temp_file.as_file().sync_all()?; 93 | 94 | Ok(temp_file.into_temp_path()) 95 | } 96 | 97 | /// Process the provided stdout output from executing an augmented script as returned by 98 | /// `script_variables_extractor` and return a map of all variables from that script. 99 | pub fn process_script_variables_extractor_output( 100 | output: impl AsRef, 101 | ) -> Result> { 102 | let output = output.as_ref(); 103 | 104 | Ok(output 105 | .lines() 106 | .filter_map(|l| l.strip_prefix("-- ")) 107 | .skip_while(|&l| l != "VARIABLE_DUMP_START") 108 | .skip(1) 109 | .map(|l| { 110 | if let Some((name, value)) = l.split_once('=') { 111 | (name.to_owned(), value.to_owned()) 112 | } else { 113 | (l.to_owned(), String::new()) 114 | } 115 | }) 116 | .collect()) 117 | } 118 | 119 | /// The cmake executable used. 120 | pub fn cmake() -> OsString { 121 | env::var_os("CMAKE").unwrap_or_else(|| "cmake".into()) 122 | } 123 | 124 | impl TryFrom<&file_api::codemodel::target::Link> for LinkArgsBuilder { 125 | type Error = Error; 126 | 127 | fn try_from(link: &file_api::codemodel::target::Link) -> Result { 128 | let linkflags = link 129 | .command_fragments 130 | .iter() 131 | .flat_map(|f| NativeCommandArgs::new(&f.fragment)) 132 | .collect(); 133 | Ok(LinkArgsBuilder { 134 | linkflags, 135 | ..Default::default() 136 | }) 137 | } 138 | } 139 | 140 | impl TryFrom<&file_api::codemodel::target::CompileGroup> for CInclArgs { 141 | type Error = Error; 142 | 143 | fn try_from(value: &file_api::codemodel::target::CompileGroup) -> Result { 144 | let args = value 145 | .defines 146 | .iter() 147 | .map(|d| format!("-D{}", d.define)) 148 | .chain( 149 | value 150 | .includes 151 | .iter() 152 | .map(|i| format!("\"-isystem{}\"", i.path)), 153 | ) 154 | .collect::>() 155 | .join(" "); 156 | 157 | Ok(Self { args }) 158 | } 159 | } 160 | 161 | #[cfg(test)] 162 | mod tests { 163 | use std::io::Write; 164 | 165 | use super::*; 166 | 167 | #[test] 168 | fn test_get_script_variables() { 169 | let mut script = tempfile::NamedTempFile::new().unwrap(); 170 | write!(&mut script, "set(VAR \"some string\")").unwrap(); 171 | 172 | let script_path = script.into_temp_path(); 173 | let vars = get_script_variables(&script_path).unwrap(); 174 | 175 | println!("{vars:?}"); 176 | 177 | let var = vars 178 | .iter() 179 | .map(|(k, v)| (k.as_str(), v.as_str())) 180 | .find(|&(k, _)| k == "VAR"); 181 | assert_eq!(var, Some(("VAR", "some string"))); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/cli/separate_args.rs: -------------------------------------------------------------------------------- 1 | #![allow(deprecated)] // TODO: For now 2 | 3 | /// An iterator that parses a command as windows command-line arguments and returns them 4 | /// as [`String`]s. 5 | /// 6 | /// See the MSDN document "Parsing C Command-Line Arguments" at 7 | /// 8 | /// for rules of parsing the windows command line. 9 | pub struct WindowsCommandArgs<'a> { 10 | command: &'a str, 11 | index: usize, 12 | in_first_argument: bool, 13 | arg: String, 14 | } 15 | 16 | impl<'a> WindowsCommandArgs<'a> { 17 | /// Create a new parser from `command`. 18 | /// 19 | /// The returned parser does NOT treat the first argument as a program path as 20 | /// described in the MSDN document. 21 | pub fn new(command: &'a str) -> WindowsCommandArgs<'a> { 22 | WindowsCommandArgs { 23 | command, 24 | index: 0, 25 | in_first_argument: false, 26 | arg: String::new(), 27 | } 28 | } 29 | 30 | /// Create a new parser from `command` where the first argument is a program path. 31 | /// 32 | /// The returned parser does treat the first argument as a program path. 33 | pub fn new_with_program(command: &'a str) -> WindowsCommandArgs<'a> { 34 | WindowsCommandArgs { 35 | command, 36 | index: 0, 37 | in_first_argument: true, 38 | arg: String::new(), 39 | } 40 | } 41 | } 42 | 43 | impl Iterator for WindowsCommandArgs<'_> { 44 | type Item = String; 45 | 46 | /// Parse the command as separate arguments. 47 | /// 48 | /// See the MSDN document "Parsing C Command-Line Arguments" at 49 | /// 50 | /// for rules of parsing the windows command line. 51 | fn next(&mut self) -> Option { 52 | let Self { 53 | command, 54 | index, 55 | in_first_argument, 56 | ref mut arg, 57 | } = *self; 58 | 59 | if index > command.len() { 60 | return None; 61 | } 62 | 63 | arg.clear(); 64 | 65 | let mut last_char = ' '; 66 | let mut in_quotes = false; 67 | let mut consecutive_quotes = 0; 68 | let mut backslashes = 0_u32; 69 | 70 | /// Append `backslashes` amount of `\` to the argument, or half the amount if 71 | /// `half` is `true`. Set `backslashes` to zero. 72 | fn push_backslashes(arg: &mut String, backslashes: &mut u32, half: bool) { 73 | if *backslashes > 0 { 74 | let n = (*backslashes >> (half as u32)) as usize; 75 | arg.reserve(n); 76 | for _ in 0..n { 77 | arg.push('\\'); 78 | } 79 | *backslashes = 0; 80 | } 81 | } 82 | 83 | fn push_quotes(arg: &mut String, quotes: &mut u32, in_quotes: &mut bool) { 84 | if *quotes > 0 { 85 | let n = (*quotes >> 1) as usize; 86 | let is_even = *quotes & 1 == 0; 87 | 88 | arg.reserve(n); 89 | for _ in 0..n { 90 | arg.push('"'); 91 | } 92 | *quotes = 0; 93 | *in_quotes = is_even; 94 | } 95 | } 96 | 97 | for (index, c) in command[index..].char_indices() { 98 | match c { 99 | '"' if in_first_argument => { 100 | in_quotes = !in_quotes; 101 | } 102 | '"' => { 103 | let is_backslash_escaped = backslashes % 2 == 1; 104 | push_backslashes(arg, &mut backslashes, true); 105 | 106 | if is_backslash_escaped { 107 | arg.push(c); 108 | } else if !in_quotes { 109 | in_quotes = true; 110 | } else { 111 | consecutive_quotes += 1; 112 | } 113 | } 114 | '\\' => { 115 | backslashes += 1; 116 | push_quotes(arg, &mut consecutive_quotes, &mut in_quotes); 117 | } 118 | // This filters empty arguments which doesn't really conform to the spec 119 | // but we don't need them. 120 | ' ' | '\t' if !in_quotes && matches!(last_char, ' ' | '\t') => {} 121 | ' ' | '\t' => { 122 | push_backslashes(arg, &mut backslashes, false); 123 | push_quotes(arg, &mut consecutive_quotes, &mut in_quotes); 124 | 125 | if in_quotes { 126 | arg.push(c); 127 | } else { 128 | self.index += index + 1; 129 | self.in_first_argument = false; 130 | 131 | let mut result = String::with_capacity(arg.len()); 132 | result.clone_from(arg); 133 | return Some(result); 134 | } 135 | } 136 | c => { 137 | push_backslashes(arg, &mut backslashes, false); 138 | push_quotes(arg, &mut consecutive_quotes, &mut in_quotes); 139 | arg.push(c); 140 | } 141 | } 142 | last_char = c; 143 | } 144 | push_backslashes(arg, &mut backslashes, false); 145 | push_quotes(arg, &mut consecutive_quotes, &mut in_quotes); 146 | 147 | self.index = command.len() + 1; 148 | if arg.is_empty() { 149 | None 150 | } else { 151 | let mut result = std::mem::take(arg); 152 | result.shrink_to_fit(); 153 | 154 | Some(result) 155 | } 156 | } 157 | } 158 | 159 | pub use shlex::join as join_unix_args; 160 | pub use shlex::quote as quote_unix_arg; 161 | pub use shlex::Shlex as UnixCommandArgs; 162 | 163 | #[cfg(windows)] 164 | pub type NativeCommandArgs<'a> = WindowsCommandArgs<'a>; 165 | #[cfg(unix)] 166 | pub type NativeCommandArgs<'a> = UnixCommandArgs<'a>; 167 | 168 | #[cfg(test)] 169 | mod test { 170 | use super::*; 171 | 172 | #[test] 173 | fn separate_windows_args() { 174 | let cmd = r#"C:\path\\\" a a "/\\//^.. "arg with whitespace" 'abc' '"" "'" "''" ""'""" s " """" \\\\"" \\\" \\\\\" \\\abc "rest a b "#; 175 | 176 | let args = WindowsCommandArgs::new_with_program(cmd).collect::>(); 177 | let mut iter = args.iter().map(|s| &s[..]); 178 | 179 | assert_eq!(iter.next(), Some(r"C:\path\\\ a a /\\//^..")); 180 | assert_eq!(iter.next(), Some("arg with whitespace")); 181 | assert_eq!(iter.next(), Some("'abc'")); 182 | assert_eq!(iter.next(), Some("'")); 183 | assert_eq!(iter.next(), Some("'")); 184 | assert_eq!(iter.next(), Some("''")); 185 | assert_eq!(iter.next(), Some("'\" s ")); 186 | assert_eq!(iter.next(), Some("\"")); 187 | assert_eq!(iter.next(), Some(r"\\")); 188 | assert_eq!(iter.next(), Some("\\\"")); 189 | assert_eq!(iter.next(), Some(r#"\\""#)); 190 | assert_eq!(iter.next(), Some(r"\\\abc")); 191 | assert_eq!(iter.next(), Some("rest a b ")); 192 | assert_eq!(iter.next(), None); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/cli/parse_args.rs: -------------------------------------------------------------------------------- 1 | use super::{Arg, ArgDef}; 2 | 3 | /// The error when parsing an command line argument. 4 | #[derive(PartialEq, Eq, Debug)] 5 | pub enum ParseError { 6 | /// The command line argument or flag was not found. 7 | NotFound, 8 | } 9 | 10 | impl std::error::Error for ParseError {} 11 | impl std::fmt::Display for ParseError { 12 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 13 | write!(f, "{self:?}") 14 | } 15 | } 16 | 17 | /// The result of parsing an command line argument. 18 | pub type Result = std::result::Result; 19 | 20 | impl super::ArgDef<'_, '_> { 21 | /// Parse this argument definition from `args` at offset `i`. 22 | /// 23 | /// This will remove the element(s) from `args` at position `i` that correspond to 24 | /// this argument and return the potential value of this argument (which is also 25 | /// removed from `args`). 26 | pub fn parse(&self, i: usize, args: &mut Vec) -> Result> { 27 | let arg = &args[i]; 28 | 29 | let hyphen_count = arg.chars().take_while(|s| *s == '-').count(); 30 | for (arg_name, arg_opts) in self.iter() { 31 | if !arg_opts.is_hyphen_count(hyphen_count) { 32 | continue; 33 | } 34 | 35 | let arg = &arg[hyphen_count..]; 36 | match self.arg { 37 | Arg::Flag => { 38 | if arg_name == arg { 39 | args.remove(i); 40 | return Ok(None); 41 | } 42 | } 43 | Arg::Option => { 44 | let mut sep_len = None; 45 | 46 | if !arg.starts_with(arg_name) 47 | || !arg_opts.matches_value_sep(&arg[arg_name.len()..], &mut sep_len) 48 | { 49 | continue; 50 | } 51 | 52 | if let Some(sep_len) = sep_len { 53 | let value = arg[arg_name.len() + sep_len..].to_owned(); 54 | args.remove(i); 55 | 56 | if arg_opts.is_value_optional() && sep_len == 0 && value.is_empty() { 57 | return Ok(None); 58 | } else { 59 | return Ok(Some(value)); 60 | } 61 | } else if arg_opts.is_value_optional() 62 | // check if the next arg starts with a `-` 63 | && args[i+1..].first().iter().any(|val| val.starts_with('-')) 64 | { 65 | args.remove(i); 66 | return Ok(None); 67 | } else { 68 | let end_index = (i + 1).min(args.len() - 1); 69 | return Ok(args.drain(i..=end_index).nth(1)); 70 | } 71 | } 72 | } 73 | } 74 | 75 | Err(ParseError::NotFound) 76 | } 77 | } 78 | 79 | /// An extension trait for parsing a collection of [`ArgDef`]s from a [`Vec`] of argument 80 | /// [`String`]s. 81 | pub trait ParseFrom { 82 | /// Result type of the parsed command line argument. 83 | type R; 84 | 85 | fn parse_from(&self, args: &mut Vec) -> Self::R; 86 | } 87 | 88 | impl ParseFrom for [&ArgDef<'_, '_>; N] { 89 | type R = [Result>; N]; 90 | 91 | /// Parse all definitions from `args` remove all arguments that match any definition. 92 | fn parse_from(&self, args: &mut Vec) -> Self::R { 93 | const INIT: Result> = Err(ParseError::NotFound); 94 | let mut results = [INIT; N]; 95 | 96 | let mut i = 0; 97 | while i < args.len() { 98 | let mut removed = false; 99 | for (def_i, def) in self.iter().enumerate() { 100 | let result = def.parse(i, args); 101 | if let Ok(result) = result { 102 | removed = true; 103 | 104 | if let Ok(ref mut results) = results[def_i] { 105 | if let Some(result) = result { 106 | results.push(result); 107 | } 108 | } else { 109 | results[def_i] = Ok(result.map(|v| vec![v]).unwrap_or_else(Vec::default)); 110 | } 111 | break; 112 | } 113 | } 114 | 115 | if !removed { 116 | i += 1; 117 | } 118 | } 119 | 120 | results 121 | } 122 | } 123 | 124 | impl ParseFrom<1> for ArgDef<'_, '_> { 125 | type R = Result>; 126 | 127 | /// Parse this definition from `args` remove all arguments that match this definition. 128 | fn parse_from(&self, args: &mut Vec) -> Result> { 129 | let mut result: Result> = Err(ParseError::NotFound); 130 | 131 | let mut i = 0; 132 | while i < args.len() { 133 | let value = self.parse(i, args); 134 | 135 | if let Ok(value) = value { 136 | if let Ok(ref mut result) = result { 137 | if let Some(value) = value { 138 | result.push(value); 139 | } 140 | } else { 141 | result = Ok(value.map(|v| vec![v]).unwrap_or_else(Vec::default)); 142 | } 143 | } else { 144 | i += 1; 145 | } 146 | } 147 | 148 | result 149 | } 150 | } 151 | 152 | #[cfg(test)] 153 | mod tests { 154 | use super::super::ArgOpts; 155 | use super::*; 156 | 157 | #[test] 158 | fn parse() { 159 | let mut args = [ 160 | "arg0", 161 | "arg1", 162 | "--flag", 163 | "-f", 164 | "--f", 165 | "-flag", 166 | "-avalue1", 167 | "-a", 168 | "value2", 169 | "arg2", 170 | "--a", 171 | "value3", 172 | "-a=value4", 173 | "arg3", 174 | ] 175 | .iter() 176 | .map(|&s| s.to_owned()) 177 | .collect::>(); 178 | 179 | let flag_single_hyphen = Arg::flag("flag").with_opts(ArgOpts::SINGLE_HYPHEN); 180 | let flag_double_hyphen = Arg::flag("flag").with_opts(ArgOpts::DOUBLE_HYPHEN); 181 | let f = Arg::flag("f"); 182 | let a_no_space = Arg::option("a").with_opts(ArgOpts::VALUE_SEP_NO_SPACE); 183 | let a_space = Arg::option("a").with_opts(ArgOpts::VALUE_SEP_NEXT_ARG); 184 | let a_equals = Arg::option("a").with_opts(ArgOpts::VALUE_SEP_EQUALS); 185 | 186 | let [flag_single_hyphen, flag_double_hyphen, f, a_equals, a_no_space, a_space] = [ 187 | &flag_single_hyphen, 188 | &flag_double_hyphen, 189 | &f, 190 | &a_equals, 191 | &a_no_space, 192 | &a_space, 193 | ] 194 | .parse_from(&mut args); 195 | 196 | assert_eq!(flag_single_hyphen, Ok(vec![])); 197 | assert_eq!(flag_double_hyphen, Ok(vec![])); 198 | assert_eq!(f, Ok(vec![])); 199 | assert_eq!(a_no_space, Ok(vec!["value1".to_owned()])); 200 | assert_eq!(a_space, Ok(vec!["value2".to_owned(), "value3".to_owned()])); 201 | assert_eq!(a_equals, Ok(vec!["value4".to_owned()])); 202 | 203 | let mut iter = args.iter().map(String::as_str); 204 | assert_eq!(iter.next(), Some("arg0")); 205 | assert_eq!(iter.next(), Some("arg1")); 206 | assert_eq!(iter.next(), Some("arg2")); 207 | assert_eq!(iter.next(), Some("arg3")); 208 | assert_eq!(iter.next(), None); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /cargo-pio/src/patches/filter_exception_decoder_esp32c3_external_conf_fix.diff: -------------------------------------------------------------------------------- 1 | diff --git a/monitor/filter_exception_decoder.py b/monitor/filter_exception_decoder.py 2 | index 6a90a59..97beb63 100644 3 | --- a/monitor/filter_exception_decoder.py 4 | +++ b/monitor/filter_exception_decoder.py 5 | @@ -31,53 +31,74 @@ class Esp32ExceptionDecoder(DeviceMonitorFilter): 6 | 7 | def __call__(self): 8 | self.buffer = "" 9 | - self.backtrace_re = re.compile( 10 | - r"^Backtrace: ?((0x[0-9a-fA-F]+:0x[0-9a-fA-F]+ ?)+)\s*" 11 | - ) 12 | + # regex matches potential PC value (0x4xxxxxxx) 13 | + # Logic identical to https://github.com/espressif/esp-idf/blob/master/tools/idf_monitor_base/constants.py#L56 14 | + self.pcaddr_re = re.compile(r'0x4[0-9a-f]{7}', re.IGNORECASE) 15 | 16 | self.firmware_path = None 17 | self.addr2line_path = None 18 | self.enabled = self.setup_paths() 19 | 20 | - if self.config.get("env:" + self.environment, "build_type") != "debug": 21 | - print( 22 | + return self 23 | + 24 | + def setup_paths(self): 25 | + self.project_dir = path_to_unicode(os.path.abspath(self.project_dir)) 26 | + 27 | + self.project_strip_dir = os.environ.get("esp32_exception_decoder_project_strip_dir") 28 | + self.firmware_path = os.environ.get("esp32_exception_decoder_firmware_path") 29 | + self.addr2line_path = os.environ.get("esp32_exception_decoder_addr2line_path") 30 | + 31 | + if self.project_strip_dir is None: 32 | + self.project_strip_dir = self.project_dir 33 | + 34 | + try: 35 | + if self.firmware_path is None or self.addr2line_path is None: 36 | + # Only load if necessary, as the call is expensive 37 | + data = load_project_ide_data(self.project_dir, self.environment) 38 | + 39 | + if self.firmware_path is None: 40 | + # Only do this check when the firmware path is not externally provided 41 | + if self.config.get("env:" + self.environment, "build_type") != "debug": 42 | + print( 43 | """ 44 | Please build project in debug configuration to get more details about an exception. 45 | See https://docs.platformio.org/page/projectconf/build_configurations.html 46 | 47 | """ 48 | - ) 49 | - 50 | - return self 51 | + ) 52 | + self.firmware_path = data["prog_path"] 53 | 54 | - def setup_paths(self): 55 | - self.project_dir = path_to_unicode(os.path.abspath(self.project_dir)) 56 | - try: 57 | - data = load_project_ide_data(self.project_dir, self.environment) 58 | - self.firmware_path = data["prog_path"] 59 | if not os.path.isfile(self.firmware_path): 60 | sys.stderr.write( 61 | - "%s: firmware at %s does not exist, rebuild the project?\n" 62 | + "%s: disabling, firmware at %s does not exist, rebuild the project?\n" 63 | % (self.__class__.__name__, self.firmware_path) 64 | ) 65 | return False 66 | 67 | - cc_path = data.get("cc_path", "") 68 | - if "-gcc" in cc_path: 69 | - path = cc_path.replace("-gcc", "-addr2line") 70 | - if os.path.isfile(path): 71 | - self.addr2line_path = path 72 | - return True 73 | + if self.addr2line_path is None: 74 | + cc_path = data.get("cc_path", "") 75 | + if "-gcc" in cc_path: 76 | + self.addr2line_path = cc_path.replace("-gcc", "-addr2line") 77 | + else: 78 | + sys.stderr.write( 79 | + "%s: disabling, failed to find addr2line.\n" % self.__class__.__name__ 80 | + ) 81 | + return False 82 | + 83 | + if not os.path.isfile(self.addr2line_path): 84 | + sys.stderr.write( 85 | + "%s: disabling, addr2line at %s does not exist\n" 86 | + % (self.__class__.__name__, self.addr2line_path) 87 | + ) 88 | + return False 89 | + 90 | + return True 91 | except PlatformioException as e: 92 | sys.stderr.write( 93 | "%s: disabling, exception while looking for addr2line: %s\n" 94 | % (self.__class__.__name__, e) 95 | ) 96 | return False 97 | - sys.stderr.write( 98 | - "%s: disabling, failed to find addr2line.\n" % self.__class__.__name__ 99 | - ) 100 | - return False 101 | 102 | def rx(self, text): 103 | if not self.enabled: 104 | @@ -97,14 +118,17 @@ See https://docs.platformio.org/page/projectconf/build_configurations.html 105 | self.buffer = "" 106 | last = idx + 1 107 | 108 | - m = self.backtrace_re.match(line) 109 | - if m is None: 110 | - continue 111 | + # Output each trace on a separate line below ours 112 | + # Logic identical to https://github.com/espressif/esp-idf/blob/master/tools/idf_monitor_base/logger.py#L131 113 | + for m in re.finditer(self.pcaddr_re, line): 114 | + if m is None: 115 | + continue 116 | + 117 | + trace = self.get_backtrace(m) 118 | + if len(trace) != "": 119 | + text = text[: last] + trace + text[last :] 120 | + last += len(trace) 121 | 122 | - trace = self.get_backtrace(m) 123 | - if len(trace) != "": 124 | - text = text[: idx + 1] + trace + text[idx + 1 :] 125 | - last += len(trace) 126 | return text 127 | 128 | def get_backtrace(self, match): 129 | @@ -114,19 +138,20 @@ See https://docs.platformio.org/page/projectconf/build_configurations.html 130 | if PY2: 131 | args = [a.encode(enc) for a in args] 132 | try: 133 | - for i, addr in enumerate(match.group(1).split()): 134 | - if PY2: 135 | - addr = addr.encode(enc) 136 | - output = ( 137 | - subprocess.check_output(args + [addr]) 138 | - .decode(enc) 139 | - .strip() 140 | - ) 141 | - output = output.replace( 142 | - "\n", "\n " 143 | - ) # newlines happen with inlined methods 144 | - output = self.strip_project_dir(output) 145 | - trace += " #%-2d %s in %s\n" % (i, addr, output) 146 | + addr = match.group() 147 | + if PY2: 148 | + addr = addr.encode(enc) 149 | + output = ( 150 | + subprocess.check_output(args + [addr]) 151 | + .decode(enc) 152 | + .strip() 153 | + ) 154 | + output = output.replace( 155 | + "\n", "\n " 156 | + ) # newlines happen with inlined methods 157 | + output = self.strip_project_dir(output) 158 | + # Output the trace in yellow color so that it is easier to spot 159 | + trace += "\033[33m=> %s: %s\033[0m\n" % (addr, output) 160 | except subprocess.CalledProcessError as e: 161 | sys.stderr.write( 162 | "%s: failed to call %s: %s\n" 163 | @@ -136,8 +161,8 @@ See https://docs.platformio.org/page/projectconf/build_configurations.html 164 | 165 | def strip_project_dir(self, trace): 166 | while True: 167 | - idx = trace.find(self.project_dir) 168 | + idx = trace.find(self.project_strip_dir) 169 | if idx == -1: 170 | break 171 | - trace = trace[:idx] + trace[idx + len(self.project_dir) + 1 :] 172 | + trace = trace[:idx] + trace[idx + len(self.project_strip_dir) + 1 :] 173 | return trace 174 | -------------------------------------------------------------------------------- /src/symgen.rs: -------------------------------------------------------------------------------- 1 | use std::fs::{self, File}; 2 | use std::io::Write; 3 | use std::path::{Path, PathBuf}; 4 | use std::{env, fmt}; 5 | 6 | use anyhow::{Error, Result}; 7 | use xmas_elf::sections::{SectionData, ShType}; 8 | use xmas_elf::symbol_table::{Binding, Visibility}; 9 | use xmas_elf::{symbol_table, ElfFile}; 10 | 11 | pub const VAR_SYMBOLS_FILE: &str = "EMBUILD_GENERATED_SYMBOLS_FILE"; 12 | 13 | #[derive(Debug)] 14 | pub struct Symbol<'a> { 15 | name: &'a str, 16 | section_name: Option<&'a str>, 17 | visible: bool, 18 | global: bool, 19 | } 20 | 21 | #[derive(Debug)] 22 | pub struct Section { 23 | pub name: String, 24 | pub prefix: Option, 25 | pub mutable: bool, 26 | } 27 | 28 | impl Section { 29 | pub fn new(name: impl Into, prefix: Option, mutable: bool) -> Self { 30 | Self { 31 | name: name.into(), 32 | prefix, 33 | mutable, 34 | } 35 | } 36 | 37 | pub fn code(name: impl Into) -> Self { 38 | Self::new(name, None, false) 39 | } 40 | 41 | pub fn data(name: impl Into) -> Self { 42 | Self::new(name, None, true) 43 | } 44 | } 45 | 46 | impl<'a> Symbol<'a> { 47 | /// Get a reference to the symbol's name. 48 | pub fn name(&self) -> &'a str { 49 | self.name 50 | } 51 | 52 | /// Get a reference to the symbol's section name. 53 | pub fn section_name(&self) -> Option<&'a str> { 54 | self.section_name 55 | } 56 | 57 | /// Get a reference to the symbol's visible. 58 | pub fn visible(&self) -> bool { 59 | self.visible 60 | } 61 | 62 | /// Get a reference to the symbol's global. 63 | pub fn global(&self) -> bool { 64 | self.global 65 | } 66 | 67 | pub fn default_pointer_gen(&self) -> Option { 68 | if self.section_name().is_some() && self.global() && self.visible() { 69 | let valid_identifier = self.name().char_indices().all(|(index, ch)| { 70 | ch == '_' || index == 0 && ch.is_alphabetic() || index > 0 && ch.is_alphanumeric() 71 | }); 72 | 73 | if valid_identifier { 74 | return Some(RustPointer { 75 | name: self.name().to_owned(), 76 | mutable: true, 77 | r#type: None, 78 | }); 79 | } 80 | } 81 | 82 | None 83 | } 84 | 85 | pub fn default_sections(&self) -> Option { 86 | self.sections(&[Section::data(".bss"), Section::data(".data")]) 87 | } 88 | 89 | pub fn sections<'b>( 90 | &'b self, 91 | sections: impl IntoIterator, 92 | ) -> Option { 93 | self.default_pointer_gen().and_then(move |mut pointer| { 94 | sections 95 | .into_iter() 96 | .find(|section| self.section_name() == Some(§ion.name)) 97 | .map(|section| { 98 | if let Some(section_prefix) = §ion.prefix { 99 | pointer.name = format!("{}{}", section_prefix, pointer.name); 100 | } 101 | 102 | pointer 103 | }) 104 | }) 105 | } 106 | } 107 | 108 | #[derive(Debug, Clone)] 109 | pub struct RustPointer { 110 | pub name: String, 111 | pub mutable: bool, 112 | pub r#type: Option, 113 | } 114 | 115 | #[allow(clippy::type_complexity)] 116 | pub struct Symgen { 117 | elf: PathBuf, 118 | start_addr: u64, 119 | rust_pointer_gen: Box Fn(&Symbol<'a>) -> Option>, 120 | } 121 | 122 | impl Symgen { 123 | pub fn new(elf: impl Into, start_addr: u64) -> Self { 124 | Self::new_with_pointer_gen(elf, start_addr, |symbol| symbol.default_sections()) 125 | } 126 | 127 | pub fn new_with_pointer_gen( 128 | elf: impl Into, 129 | start_addr: u64, 130 | rust_pointer_gen: impl for<'a> Fn(&Symbol<'a>) -> Option + 'static, 131 | ) -> Self { 132 | Self { 133 | elf: elf.into(), 134 | start_addr, 135 | rust_pointer_gen: Box::new(rust_pointer_gen), 136 | } 137 | } 138 | 139 | pub fn run(&self) -> Result { 140 | let output_file = PathBuf::from(env::var("OUT_DIR")?).join("symbols.rs"); 141 | 142 | self.run_for_file(&output_file)?; 143 | 144 | println!( 145 | "cargo:rustc-env={}={}", 146 | VAR_SYMBOLS_FILE, 147 | output_file.display() 148 | ); 149 | 150 | Ok(output_file) 151 | } 152 | 153 | pub fn run_for_file(&self, output_file: impl AsRef) -> Result<()> { 154 | let output_file = output_file.as_ref(); 155 | 156 | eprintln!("Output: {output_file:?}"); 157 | 158 | self.write(&mut File::create(output_file)?) 159 | } 160 | 161 | pub fn write(&self, output: &mut impl Write) -> Result<()> { 162 | eprintln!("Input: {:?}", self.elf); 163 | 164 | let elf_data = fs::read(&self.elf)?; 165 | let elf = ElfFile::new(&elf_data).map_err(Error::msg)?; 166 | 167 | for symtable in self.get_symtables(&elf) { 168 | match symtable.1 { 169 | SectionData::SymbolTable32(entries) => { 170 | self.write_symbols(&elf, symtable.0, entries.iter().enumerate(), output)? 171 | } 172 | SectionData::SymbolTable64(entries) => { 173 | self.write_symbols(&elf, symtable.0, entries.iter().enumerate(), output)? 174 | } 175 | _ => unimplemented!(), 176 | } 177 | } 178 | 179 | Ok(()) 180 | } 181 | 182 | fn write_symbols<'a, W: Write>( 183 | &self, 184 | elf: &'a ElfFile<'a>, 185 | symtable_index: usize, 186 | symbols: impl Iterator, 187 | output: &mut W, 188 | ) -> Result<()> { 189 | for (_index, sym) in symbols { 190 | eprintln!("Found symbol: {sym:?}"); 191 | 192 | let sym_type = sym.get_type().map_err(Error::msg)?; 193 | 194 | if sym_type == symbol_table::Type::Object || sym_type == symbol_table::Type::NoType { 195 | let name = sym.get_name(elf).map_err(Error::msg)?; 196 | 197 | let section_name = sym 198 | .get_section_header(elf, symtable_index) 199 | .and_then(|sh| sh.get_name(elf)) 200 | .ok(); 201 | 202 | let global = sym.get_binding().map_err(Error::msg)? == Binding::Global; 203 | let visible = matches!(sym.get_other(), Visibility::Default); 204 | 205 | let symbol = Symbol { 206 | name, 207 | section_name, 208 | global, 209 | visible, 210 | }; 211 | 212 | let pointer = (self.rust_pointer_gen)(&symbol); 213 | 214 | if let Some(pointer) = pointer { 215 | eprintln!("Writing symbol: {name} [{symbol:?}] as [{pointer:?}]"); 216 | write!( 217 | output, 218 | "#[allow(dead_code, non_upper_case_globals)]\npub const {name}: *{mutable} {typ} = 0x{addr:x} as *{mutable} {typ};\n", 219 | name = pointer.name, 220 | mutable = if pointer.mutable { "mut" } else {"const" }, 221 | typ = pointer.r#type.unwrap_or_else(|| "core::ffi::c_void".to_owned()), 222 | addr = self.start_addr + sym.value() 223 | )?; 224 | } else { 225 | eprintln!("Skipping symbol: {name} [{sym:?}]"); 226 | } 227 | } 228 | } 229 | 230 | Ok(()) 231 | } 232 | 233 | fn get_symtables<'a, 'b>( 234 | &self, 235 | elf: &'b ElfFile<'a>, 236 | ) -> impl Iterator)> + 'b { 237 | elf.section_iter() 238 | .enumerate() 239 | .filter(|(_, header)| header.get_type().map_err(Error::msg).unwrap() == ShType::SymTab) 240 | .map(move |(index, header)| (index, header.get_data(elf).unwrap())) 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /src/espidf/ulp_fsm.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{OsStr, OsString}; 2 | use std::path::{Path, PathBuf}; 3 | use std::{env, iter}; 4 | 5 | use crate::build::CInclArgs; 6 | use crate::utils::OsStrExt; 7 | use crate::{symgen, *}; 8 | 9 | #[derive(Clone, Debug)] 10 | pub enum SystemIncludes { 11 | CInclArgs(CInclArgs), 12 | MCU(String), 13 | } 14 | 15 | #[derive(Clone, Debug)] 16 | pub struct BuildResult { 17 | pub bin_file: PathBuf, 18 | pub elf_file: PathBuf, 19 | pub sym_rs_file: PathBuf, 20 | } 21 | 22 | pub struct Builder { 23 | esp_idf: PathBuf, 24 | sys_includes: SystemIncludes, 25 | add_includes: Vec, 26 | gcc: Option, 27 | env_path: Option, 28 | } 29 | 30 | impl Builder { 31 | pub fn try_from_embuild_env( 32 | library: impl AsRef, 33 | add_includes: impl Into>, 34 | ) -> anyhow::Result { 35 | let library = library.as_ref(); 36 | 37 | Ok(Self { 38 | esp_idf: PathBuf::from(env::var(format!("DEP_{library}_EMBUILD_ESP_IDF_PATH"))?), 39 | sys_includes: SystemIncludes::CInclArgs(build::CInclArgs::try_from_env(library)?), 40 | add_includes: add_includes.into(), 41 | gcc: None, 42 | env_path: env::var_os("DEP_ESP_IDF_EMBUILD_ENV_PATH"), 43 | }) 44 | } 45 | 46 | pub fn new( 47 | esp_idf: impl Into, 48 | sys_includes: SystemIncludes, 49 | add_includes: impl Into>, 50 | gcc: Option, 51 | env_path: Option, 52 | ) -> Self { 53 | Self { 54 | esp_idf: esp_idf.into(), 55 | sys_includes, 56 | add_includes: add_includes.into(), 57 | gcc, 58 | env_path, 59 | } 60 | } 61 | 62 | pub fn build<'a, I>( 63 | &self, 64 | ulp_sources: I, 65 | out_dir: impl AsRef, 66 | ) -> anyhow::Result 67 | where 68 | I: IntoIterator, 69 | { 70 | let out_dir = out_dir.as_ref(); 71 | 72 | let include_args = self.include_args(); 73 | 74 | let ulp_obj_out_dir = path_buf![&out_dir, "obj"]; 75 | 76 | self.compile(ulp_sources, &include_args, &ulp_obj_out_dir)?; 77 | 78 | let ulp_ld_script = ["ulp_fsm.ld", "esp32.ulp.ld"] 79 | .into_iter() 80 | .map(|ulp_file_name| path_buf![&self.esp_idf, "components", "ulp", "ld", ulp_file_name]) 81 | .find(|ulp_path| ulp_path.exists()) 82 | .ok_or_else(|| anyhow::anyhow!("Cannot find the ULP FSM LD script in ESP-IDF"))?; 83 | 84 | let ulp_ld_out_script = path_buf![&out_dir, "ulp.ld"]; 85 | 86 | self.preprocess_one(&ulp_ld_script, &include_args, &ulp_ld_out_script)?; 87 | 88 | let ulp_elf = path_buf![&out_dir, "ulp"]; 89 | 90 | self.link(&ulp_obj_out_dir, &ulp_ld_out_script, &ulp_elf)?; 91 | 92 | let ulp_bin = path_buf![&out_dir, "ulp.bin"]; 93 | 94 | self.bin(&ulp_elf, &ulp_bin)?; 95 | 96 | let ulp_sym_rs = path_buf![&out_dir, "ulp.rs"]; 97 | 98 | self.symbolize(&ulp_elf, &ulp_sym_rs)?; 99 | 100 | Ok(BuildResult { 101 | bin_file: ulp_bin, 102 | elf_file: ulp_elf, 103 | sym_rs_file: ulp_sym_rs, 104 | }) 105 | } 106 | 107 | fn compile<'a, I>( 108 | &self, 109 | ulp_sources: I, 110 | include_args: &[impl AsRef], 111 | out_dir: &Path, 112 | ) -> anyhow::Result<()> 113 | where 114 | I: IntoIterator, 115 | { 116 | for ulp_source in ulp_sources { 117 | std::fs::create_dir_all(out_dir)?; 118 | 119 | let ulp_preprocessed_source = Self::resuffix(ulp_source, out_dir, "ulp.S")?; 120 | 121 | self.preprocess_one(ulp_source, include_args, &ulp_preprocessed_source)?; 122 | 123 | let ulp_object = Self::resuffix(ulp_source, out_dir, "o")?; 124 | 125 | self.compile_one(&ulp_preprocessed_source, &ulp_object)?; 126 | } 127 | 128 | Ok(()) 129 | } 130 | 131 | fn compile_one(&self, ulp_source: &Path, out_file: &Path) -> anyhow::Result<()> { 132 | cmd![self.tool("esp32ulp-elf-as")?, "-o", out_file, ulp_source].run()?; 133 | 134 | Ok(()) 135 | } 136 | 137 | fn preprocess_one( 138 | &self, 139 | source: &Path, 140 | include_args: &[impl AsRef], 141 | out_file: &Path, 142 | ) -> anyhow::Result<()> { 143 | cmd![ 144 | self.tool(self.gcc.as_deref().unwrap_or("xtensa-esp32-elf-gcc"))?, 145 | "-E", 146 | "-P", 147 | "-xc", 148 | "-D__ASSEMBLER__", 149 | @include_args, 150 | "-o", 151 | out_file, 152 | source 153 | ] 154 | .run()?; 155 | 156 | Ok(()) 157 | } 158 | 159 | fn link( 160 | &self, 161 | object_files_dir: &Path, 162 | linker_script: &Path, 163 | out_file: &Path, 164 | ) -> anyhow::Result<()> { 165 | let object_files = std::fs::read_dir(object_files_dir)? 166 | .filter_map(|file| { 167 | file.ok() 168 | .filter(|file| file.path().extension().map(|e| e == "o").unwrap_or(false)) 169 | }) 170 | .map(|de| de.path().as_os_str().to_owned()) 171 | .collect::>(); 172 | 173 | cmd![ 174 | self.tool("esp32ulp-elf-ld")?, 175 | "-T", 176 | linker_script, 177 | @object_files, 178 | "-o", 179 | out_file 180 | ] 181 | .run()?; 182 | 183 | Ok(()) 184 | } 185 | 186 | fn bin(&self, ulp_elf: &Path, out_file: &Path) -> anyhow::Result<()> { 187 | // TODO: Switch to our own bingen in embuild 188 | cmd![ 189 | self.tool("esp32ulp-elf-objcopy")?, 190 | ulp_elf, 191 | "-O", 192 | "binary", 193 | out_file 194 | ] 195 | .run()?; 196 | 197 | Ok(()) 198 | } 199 | 200 | fn symbolize(&self, ulp_elf: &Path, out_file: &Path) -> anyhow::Result<()> { 201 | symgen::Symgen::new_with_pointer_gen(ulp_elf, 0x5000_0000_u64, |symbol| { 202 | symbol 203 | .sections(&[ 204 | symgen::Section::code(".text"), 205 | symgen::Section::data(".bss"), 206 | symgen::Section::data(".data"), 207 | ]) 208 | .map(|mut pointer| { 209 | pointer.r#type = Some("u32".to_owned()); 210 | pointer 211 | }) 212 | }) 213 | .run_for_file(out_file) 214 | } 215 | 216 | fn include_args(&self) -> Vec { 217 | match self.sys_includes { 218 | SystemIncludes::CInclArgs(ref include_args) => self 219 | .add_includes 220 | .iter() 221 | .cloned() 222 | .chain( 223 | include_args 224 | .args 225 | .split_ascii_whitespace() 226 | .filter_map(Self::unescape), 227 | ) 228 | .collect::>(), 229 | SystemIncludes::MCU(ref mcu) => self 230 | .add_includes 231 | .iter() 232 | .cloned() 233 | .chain(iter::once(format!( 234 | "{}", 235 | path_buf![&self.esp_idf, "components", "soc", mcu].display() 236 | ))) 237 | .flat_map(|s| iter::once("-I".to_owned()).chain(iter::once(s))) 238 | .collect::>(), 239 | } 240 | } 241 | 242 | fn unescape(arg: &str) -> Option { 243 | let unescaped = if arg.starts_with('\"') && arg.ends_with('\"') { 244 | arg[1..arg.len() - 1].replace("\\\"", "\"") 245 | } else { 246 | arg.to_owned() 247 | }; 248 | 249 | if unescaped.starts_with("-isystem") || unescaped.starts_with("-I") { 250 | Some(unescaped) 251 | } else { 252 | None 253 | } 254 | } 255 | 256 | fn resuffix(path: &Path, out_dir: &Path, suffix: &str) -> anyhow::Result { 257 | let resuffixed = path_buf![ 258 | &out_dir, 259 | format!( 260 | "{}.{}", 261 | path.file_stem() 262 | .ok_or_else(|| anyhow::anyhow!("Wrong file name {}", path.display()))? 263 | .try_to_str()?, 264 | suffix 265 | ) 266 | ]; 267 | 268 | Ok(resuffixed) 269 | } 270 | 271 | fn tool(&self, tool: &str) -> anyhow::Result { 272 | let path = which::which_in(tool, self.env_path.clone(), env::current_dir()?)?; 273 | 274 | Ok(path) 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /src/cmd.rs: -------------------------------------------------------------------------------- 1 | //! Command building and running utilities. 2 | 3 | use std::ffi::OsStr; 4 | use std::io; 5 | use std::process::{self, Command, ExitStatus}; 6 | 7 | /// Error when trying to execute a command. 8 | #[derive(Debug, thiserror::Error)] 9 | pub enum CmdError { 10 | /// The command failed to start. 11 | #[error("command '{0}' failed to start")] 12 | NoRun(String, #[source] io::Error), 13 | /// The command exited unsucessfully (with non-zero exit status). 14 | #[error("command '{0}' exited with non-zero status code {1}")] 15 | Unsuccessful(String, i32, #[source] Option), 16 | /// The command was terminated unexpectedly. 17 | #[error("command '{0}' was terminated unexpectedly")] 18 | Terminated(String), 19 | } 20 | 21 | impl CmdError { 22 | /// Create a [`CmdError::NoRun`]. 23 | pub fn no_run(cmd: &process::Command, error: io::Error) -> Self { 24 | CmdError::NoRun(format!("{cmd:?}"), error) 25 | } 26 | 27 | /// Convert a [`process::ExitStatus`] into a `Result<(), CmdError>`. 28 | pub fn status_into_result( 29 | status: process::ExitStatus, 30 | cmd: &process::Command, 31 | cmd_output: impl FnOnce() -> Option, 32 | ) -> Result<(), Self> { 33 | if status.success() { 34 | Ok(()) 35 | } else if let Some(code) = status.code() { 36 | Err(CmdError::Unsuccessful( 37 | format!("{cmd:?}"), 38 | code, 39 | cmd_output().map(anyhow::Error::msg), 40 | )) 41 | } else { 42 | Err(CmdError::Terminated(format!("{cmd:?}"))) 43 | } 44 | } 45 | } 46 | 47 | /// A wrapper over a [`std::process::Command`] with more features. 48 | #[derive(Debug)] 49 | pub struct Cmd { 50 | /// The actual [`std::process::Command`] wrapped. 51 | pub cmd: std::process::Command, 52 | ignore_exitcode: bool, 53 | } 54 | 55 | impl std::ops::Deref for Cmd { 56 | type Target = std::process::Command; 57 | 58 | fn deref(&self) -> &Self::Target { 59 | &self.cmd 60 | } 61 | } 62 | 63 | impl std::ops::DerefMut for Cmd { 64 | fn deref_mut(&mut self) -> &mut Self::Target { 65 | &mut self.cmd 66 | } 67 | } 68 | 69 | impl From for Cmd { 70 | fn from(cmd: std::process::Command) -> Self { 71 | Cmd { 72 | cmd, 73 | ignore_exitcode: false, 74 | } 75 | } 76 | } 77 | 78 | impl From for std::process::Command { 79 | fn from(cmd: Cmd) -> Self { 80 | cmd.into_inner() 81 | } 82 | } 83 | 84 | impl Cmd { 85 | /// Construct a new [`Cmd`] for launching `program` (see 86 | /// [`std::process::Command::new`]). 87 | pub fn new(program: impl AsRef) -> Self { 88 | Self { 89 | cmd: Command::new(program), 90 | ignore_exitcode: false, 91 | } 92 | } 93 | 94 | /// Ignore the exit code when executing this command. 95 | /// 96 | /// Applies to: 97 | /// - [`Cmd::run`] 98 | /// - [`Cmd::output`] 99 | /// - [`Cmd::stdout`] 100 | /// - [`Cmd::stderr`] 101 | pub fn ignore_exitcode(&mut self) -> &mut Self { 102 | self.ignore_exitcode = true; 103 | self 104 | } 105 | 106 | /// Run the command to completion. 107 | /// 108 | /// If [`Cmd::ignore_exitcode`] has been called a program that exited with an error 109 | /// will also return [`Ok`], otherwise it will return [`Err`]. 110 | /// A program that failed to start will always return an [`Err`]. 111 | /// 112 | /// [`std::process::Command::status`] is used internally. 113 | pub fn run(&mut self) -> Result<(), CmdError> { 114 | self.cmd 115 | .status() 116 | .map_err(|e| CmdError::no_run(&self.cmd, e)) 117 | .and_then(|v| { 118 | if self.ignore_exitcode { 119 | Ok(()) 120 | } else { 121 | CmdError::status_into_result(v, &self.cmd, || None) 122 | } 123 | }) 124 | } 125 | 126 | /// Run the command and get its [`ExitStatus`]. 127 | pub fn status(&mut self) -> Result { 128 | self.cmd 129 | .status() 130 | .map_err(|e| CmdError::no_run(&self.cmd, e)) 131 | } 132 | 133 | fn print_output(&self, output: &std::process::Output) { 134 | // TODO: add some way to quiet this output 135 | use std::io::Write; 136 | std::io::stdout().write_all(&output.stdout[..]).ok(); 137 | std::io::stderr().write_all(&output.stderr[..]).ok(); 138 | } 139 | 140 | /// Run the command to completion and use its [`std::process::Output`] with `func`. 141 | /// 142 | /// If [`Cmd::ignore_exitcode`] has been called a program that exited with an error 143 | /// will also return [`Ok`], otherwise it will return [`Err`]. 144 | /// A program that failed to start will always return an [`Err`]. 145 | /// 146 | /// [`std::process::Command::output`] is used internally. 147 | pub fn output( 148 | &mut self, 149 | func: impl FnOnce(std::process::Output) -> T, 150 | ) -> Result { 151 | match self.cmd.output() { 152 | Err(err) => Err(CmdError::no_run(&self.cmd, err)), 153 | Ok(result) => if self.ignore_exitcode { 154 | self.print_output(&result); 155 | Ok(()) 156 | } else { 157 | CmdError::status_into_result(result.status, &self.cmd, || { 158 | Some( 159 | String::from_utf8_lossy(&result.stderr[..]) 160 | .trim_end() 161 | .to_string(), 162 | ) 163 | }) 164 | } 165 | .map_err(|e| { 166 | self.print_output(&result); 167 | e 168 | }) 169 | .map(|_| func(result)), 170 | } 171 | } 172 | 173 | /// Run the command to completion and get its stdout output. 174 | /// 175 | /// See [`Cmd::output`]. 176 | pub fn stdout(&mut self) -> Result { 177 | self.output(|output| { 178 | String::from_utf8_lossy(&output.stdout[..]) 179 | .trim_end() 180 | .to_string() 181 | }) 182 | } 183 | 184 | /// Run the command to completion and get its stderr output. 185 | /// 186 | /// See [`Cmd::output`]. 187 | pub fn stderr(&mut self) -> Result { 188 | self.output(|output| { 189 | String::from_utf8_lossy(&output.stderr[..]) 190 | .trim_end() 191 | .to_string() 192 | }) 193 | } 194 | 195 | /// Turn this [`Cmd`] into its underlying [`std::process::Command`]. 196 | pub fn into_inner(self) -> std::process::Command { 197 | self.cmd 198 | } 199 | } 200 | 201 | /// Build a command using a given [`std::process::Command`] or [`Cmd`] and return it. 202 | /// 203 | /// The first argument is expected to be a [`std::process::Command`] or [`Cmd`] instance. 204 | /// 205 | /// For a `new` builder the second argument, the program to run (passed to 206 | /// [`std::process::Command::new`]) is mandatory. Every comma seperated argument 207 | /// thereafter is added to the command's arguments. Arguments after an `@`-sign specify 208 | /// collections of arguments (specifically `impl IntoIterator`). 209 | /// The opional `key=value` arguments after a semicolon are simply translated to calling 210 | /// the `std::process::Command::` method with `value` as its arguments. 211 | /// 212 | /// **Note:** 213 | /// `@`-arguments must be followed by at least one normal argument. For example 214 | /// `cmd_build!(new, "cmd", @args)` will not compile but `cmd_build!(new, "cmd", @args, 215 | /// "other")` will. You can use `key=value` arguments to work around this limitation: 216 | /// `cmd_build!(new, "cmd"; args=(args))`. 217 | /// 218 | /// At the end the built [`std::process::Command`] is returned. 219 | /// 220 | /// # Examples 221 | /// ``` 222 | /// # use embuild::{cmd::Cmd, cmd_build}; 223 | /// let args_list = ["--foo", "--bar", "value"]; 224 | /// let mut cmd = Cmd::new("git"); 225 | /// let mut cmd = cmd_build!(cmd, @args_list, "clone"; arg=("url.com"), env=("var", "value")); 226 | /// ``` 227 | #[macro_export] 228 | macro_rules! cmd_build { 229 | ($builder:ident $(, $(@$cmdargs:expr,)* $cmdarg:expr)* $(; $($k:ident = $v:tt),*)?) => {{ 230 | $( 231 | $($builder .args($cmdargs);)* 232 | $builder .arg($cmdarg); 233 | )* 234 | $($($builder . $k $v;)*)? 235 | 236 | $builder 237 | }} 238 | } 239 | 240 | /// Create a new [`Cmd`] instance. 241 | /// 242 | /// This is a simple wrapper over the [`std::process::Command`] and [`Cmd`] API. It 243 | /// expects at least one argument for the program to run. Every comma seperated argument 244 | /// thereafter is added to the command's arguments. Arguments after an `@`-sign specify 245 | /// collections of arguments (specifically `impl IntoIterator`). 246 | /// The opional `key=value` arguments after a semicolon are simply translated to calling 247 | /// the `Cmd::` method with `value` as its arguments. 248 | /// 249 | /// **Note:** 250 | /// `@`-arguments must be followed by at least one normal argument. For example 251 | /// `cmd!("cmd", @args)` will not compile but `cmd!("cmd", @args, "other")` will. You can 252 | /// use `key=value` arguments to work around this limitation: `cmd!("cmd"; args=(args))`. 253 | /// 254 | /// # Examples 255 | /// ``` 256 | /// # use embuild::cmd; 257 | /// let args_list = ["--foo", "--bar", "value"]; 258 | /// let mut cmd = cmd!("git", @args_list, "clone"; arg=("url.com"), env=("var", "value")); 259 | /// ``` 260 | #[macro_export] 261 | macro_rules! cmd { 262 | ($cmd:expr $(, $(@$cmdargs:expr,)* $cmdarg:expr)* $(; $($k:ident = $v:tt),*)?) => {{ 263 | let mut cmd = $crate::cmd::Cmd::new($cmd); 264 | $crate::cmd_build!(cmd $(, $(@$cmdargs,)* $cmdarg)* $(; $($k = $v),* )?) 265 | }}; 266 | } 267 | -------------------------------------------------------------------------------- /src/cmake/file_api/index.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::convert::TryFrom; 3 | use std::fs; 4 | use std::path::PathBuf; 5 | 6 | use anyhow::{anyhow, bail, Context, Result}; 7 | use serde::{Deserialize, Serialize}; 8 | use serde_json::Value; 9 | 10 | use super::cache::Cache; 11 | use super::codemodel::Codemodel; 12 | use super::toolchains::Toolchains; 13 | use super::{Query, Version}; 14 | 15 | /// CMake tool kind for [`CMake::paths`]. 16 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)] 17 | pub enum PathsKey { 18 | #[serde(rename = "cmake")] 19 | CMake, 20 | #[serde(rename = "ctest")] 21 | CTest, 22 | #[serde(rename = "cpack")] 23 | CPack, 24 | #[serde(rename = "root")] 25 | Root, 26 | } 27 | 28 | /// The cmake generator used for the build. 29 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] 30 | #[serde(rename_all = "camelCase")] 31 | pub struct Generator { 32 | /// Whether the generator supports multiple output configurations. 33 | pub multi_config: bool, 34 | /// The name of the generator. 35 | pub name: String, 36 | /// If the generator supports `CMAKE_GENERATOR_PLATFORM`, specifies the generator 37 | /// platform name. 38 | pub platform: Option, 39 | } 40 | 41 | /// Information about the instance of CMake that generated the replies. 42 | #[derive(Debug, Clone, Deserialize)] 43 | #[serde(rename_all = "camelCase")] 44 | pub struct CMake { 45 | pub version: Version, 46 | pub paths: HashMap, 47 | pub generator: Generator, 48 | } 49 | 50 | /// CMake file API object kind for which cmake should generate information about. 51 | #[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone, Copy, Hash)] 52 | #[serde(rename_all = "camelCase")] 53 | pub enum ObjKind { 54 | Codemodel, 55 | Cache, 56 | CmakeFiles, 57 | Toolchains, 58 | } 59 | 60 | impl ObjKind { 61 | /// Get the supported major version of this object kind. 62 | pub(crate) const fn supported_version(self) -> u32 { 63 | match self { 64 | Self::Codemodel => 2, 65 | Self::Cache => 2, 66 | Self::CmakeFiles => 1, 67 | Self::Toolchains => 1, 68 | } 69 | } 70 | 71 | /// Check if `object_version` is supported by this library. 72 | pub fn check_version_supported(self, object_version: u32) -> Result<()> { 73 | let expected_version = self.supported_version(); 74 | if object_version != expected_version { 75 | bail!( 76 | "cmake {} object version not supported (expected {}, got {})", 77 | self.as_str(), 78 | expected_version, 79 | object_version 80 | ); 81 | } else { 82 | Ok(()) 83 | } 84 | } 85 | 86 | /// Get the minimum required cmake version for this object kind. 87 | pub fn min_cmake_version(self) -> Version { 88 | let (major, minor) = match self { 89 | Self::Codemodel => (3, 14), 90 | Self::Cache => (3, 14), 91 | Self::CmakeFiles => (3, 14), 92 | Self::Toolchains => (3, 20), 93 | }; 94 | Version { 95 | major, 96 | minor, 97 | ..Version::default() 98 | } 99 | } 100 | 101 | pub fn as_str(self) -> &'static str { 102 | match self { 103 | Self::Codemodel => "codemodel", 104 | Self::Cache => "cache", 105 | Self::CmakeFiles => "cmakeFiles", 106 | Self::Toolchains => "toolchains", 107 | } 108 | } 109 | } 110 | 111 | /// A reply for a specific object kind. 112 | #[derive(Debug, Clone, Deserialize)] 113 | #[serde(rename_all = "camelCase")] 114 | pub struct Reply { 115 | /// Path to the JSON file which contains the object. 116 | pub json_file: PathBuf, 117 | /// The kind of cmake file API object. 118 | pub kind: ObjKind, 119 | /// The version of the generated object. 120 | pub version: Version, 121 | } 122 | 123 | impl Reply { 124 | /// Try to load this reply as a codemodel object. 125 | pub fn codemodel(&self) -> Result { 126 | Codemodel::try_from(self) 127 | } 128 | 129 | /// Try to load this reply as a cache object. 130 | pub fn cache(&self) -> Result { 131 | Cache::try_from(self) 132 | } 133 | 134 | /// Try to load this reply as a toolchains object. 135 | pub fn toolchains(&self) -> Result { 136 | Toolchains::try_from(self) 137 | } 138 | } 139 | 140 | /// Replies generated from a cmake file API query. 141 | #[derive(Debug, Clone)] 142 | pub struct Replies { 143 | /// Information about the instance of CMake that generated the replies. 144 | pub cmake: CMake, 145 | /// All generated replies. 146 | pub replies: HashMap, 147 | } 148 | 149 | impl Replies { 150 | /// Try to load the cmake file API index from the query and validate. 151 | pub fn from_query(query: &Query) -> Result { 152 | let reply_dir = query.api_dir.join("reply"); 153 | 154 | let index_file = fs::read_dir(&reply_dir) 155 | .context("Failed to list cmake-file-api reply directory")? 156 | .filter_map( 157 | |file| match (&file, file.as_ref().ok().and_then(|f| f.file_type().ok())) { 158 | (Ok(f), Some(file_type)) 159 | if file_type.is_file() 160 | && f.file_name().to_string_lossy().starts_with("index-") => 161 | { 162 | Some(f.path()) 163 | } 164 | _ => None, 165 | }, 166 | ) 167 | .max() 168 | .ok_or_else(|| { 169 | anyhow!( 170 | "No cmake-file-api index file found in '{}' \ 171 | (cmake version must be at least 3.14)", 172 | reply_dir.display() 173 | ) 174 | })?; 175 | 176 | #[derive(Deserialize)] 177 | struct Index { 178 | cmake: CMake, 179 | reply: HashMap, 180 | } 181 | 182 | let base_error = || { 183 | anyhow!( 184 | "Failed to parse the cmake-file-api index file '{}'", 185 | index_file.display() 186 | ) 187 | }; 188 | let Index { cmake, reply } = 189 | serde_json::from_reader(&fs::File::open(&index_file)?).with_context(base_error)?; 190 | 191 | for kind in query.kinds { 192 | let min_cmake_version = kind.min_cmake_version(); 193 | if cmake.version.major < min_cmake_version.major 194 | || cmake.version.major == min_cmake_version.major 195 | && cmake.version.minor < min_cmake_version.minor 196 | { 197 | bail!( 198 | "cmake-file-api {} object not supported: cmake version missmatch, \ 199 | expected at least version {}, got version {} instead", 200 | kind.as_str(), 201 | min_cmake_version, 202 | &cmake.version 203 | ); 204 | } 205 | } 206 | 207 | let client = format!("client-{}", &query.client_name); 208 | let (_, reply) = reply 209 | .into_iter() 210 | .find(|(k, _)| k == &client) 211 | .ok_or_else(|| anyhow!("Reply for client '{}' not found", &query.client_name)) 212 | .with_context(base_error)?; 213 | 214 | #[derive(Deserialize)] 215 | #[serde(untagged)] 216 | enum ReplyOrError { 217 | Reply(Reply), 218 | Error { error: String }, 219 | } 220 | 221 | let mut errors = vec![]; 222 | let replies: HashMap = 223 | serde_json::from_value::>(reply) 224 | .with_context(base_error)? 225 | .into_iter() 226 | .filter_map(|(_, v)| match v { 227 | ReplyOrError::Reply(mut r) => { 228 | if let Err(err) = r.kind.check_version_supported(r.version.major) { 229 | errors.push(err.to_string()); 230 | None 231 | } else { 232 | r.json_file = reply_dir.join(r.json_file); 233 | Some((r.kind, r)) 234 | } 235 | } 236 | ReplyOrError::Error { error } => { 237 | errors.push(error); 238 | None 239 | } 240 | }) 241 | .collect(); 242 | 243 | let not_found = query 244 | .kinds 245 | .iter() 246 | .filter(|k| !replies.contains_key(k)) 247 | .map(|k| k.as_str()) 248 | .collect::>(); 249 | 250 | if !not_found.is_empty() { 251 | let error = anyhow!( 252 | "Objects {} could not be deserialized{}", 253 | not_found.join(", "), 254 | if errors.is_empty() { 255 | String::new() 256 | } else { 257 | format!(":\n{}", errors.join(",\n")) 258 | } 259 | ); 260 | return Err(error 261 | .context(format!( 262 | "Could not deserialize all requested objects ({:?})", 263 | query.kinds 264 | )) 265 | .context(base_error())); 266 | } else if !errors.is_empty() { 267 | log::debug!( 268 | "Errors while deserializing cmake-file-api index `{:?}`: {}", 269 | index_file, 270 | errors.join(",\n") 271 | ); 272 | } 273 | 274 | Ok(Replies { cmake, replies }) 275 | } 276 | 277 | /// Get a reply of `kind`. 278 | pub fn get_kind(&self, kind: ObjKind) -> Result<&Reply> { 279 | self.replies.get(&kind).ok_or_else(|| { 280 | anyhow!( 281 | "Object {:?} (version {}) not fund in cmake-file-api reply index", 282 | kind, 283 | kind.supported_version() 284 | ) 285 | }) 286 | } 287 | 288 | /// Load the codemodel object from a codemodel reply. 289 | /// 290 | /// Convenience function for `get_kind(ObjKind::Codemodel)?.codemodel()`. 291 | pub fn get_codemodel(&self) -> Result { 292 | self.get_kind(ObjKind::Codemodel)?.codemodel() 293 | } 294 | 295 | /// Load the cache object from a cache reply. 296 | /// 297 | /// Convenience function for `get_kind(ObjKind::Cache)?.cache()`. 298 | pub fn get_cache(&self) -> Result { 299 | self.get_kind(ObjKind::Cache)?.cache() 300 | } 301 | 302 | /// Load the toolchains object from a toolchains reply. 303 | /// 304 | /// Convenience function for `get_kind(ObjKind::Toolchains)?.toolchains()`. 305 | pub fn get_toolchains(&self) -> Result { 306 | self.get_kind(ObjKind::Toolchains)?.toolchains() 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /src/cmake/file_api/codemodel.rs: -------------------------------------------------------------------------------- 1 | //! Codemodel cmake file API object. 2 | //! 3 | //! The codemodel object kind describes the build system structure as modeled by CMake. 4 | 5 | use std::convert::TryFrom; 6 | use std::fs; 7 | use std::path::PathBuf; 8 | use std::sync::Arc; 9 | 10 | use anyhow::{anyhow, Context, Error, Result}; 11 | use serde::Deserialize; 12 | 13 | use super::index::{self, ObjKind}; 14 | use super::Version; 15 | 16 | /// The description of the build system structure as modeled by CMake. 17 | #[derive(Debug, Deserialize, Clone)] 18 | pub struct Codemodel { 19 | #[serde(skip)] 20 | codemodel_dir: Arc, 21 | /// The version of this object kind. 22 | pub version: Version, 23 | /// Some paths used by cmake. 24 | pub paths: Paths, 25 | /// All available build configurations. 26 | /// 27 | /// On single-configuration generators there is one entry for the value of the 28 | /// `CMAKE_BUILD_TYPE` variable. For multi-configuration generators there is an entry for 29 | /// each configuration listed in the `CMAKE_CONFIGURATION_TYPES` variable. 30 | pub configurations: Vec, 31 | } 32 | 33 | impl TryFrom<&index::Reply> for Codemodel { 34 | type Error = Error; 35 | fn try_from(value: &index::Reply) -> Result { 36 | assert!(value.kind == ObjKind::Codemodel); 37 | ObjKind::Codemodel 38 | .check_version_supported(value.version.major) 39 | .unwrap(); 40 | 41 | let mut codemodel: Codemodel = serde_json::from_reader(&fs::File::open(&value.json_file)?) 42 | .with_context(|| { 43 | anyhow!( 44 | "Parsing cmake-file-api codemodel object file '{}' failed", 45 | value.json_file.display() 46 | ) 47 | })?; 48 | 49 | codemodel.codemodel_dir = Arc::new(value.json_file.parent().unwrap().to_owned()); 50 | for conf in codemodel.configurations.iter_mut() { 51 | conf.codemodel_dir = codemodel.codemodel_dir.clone(); 52 | } 53 | 54 | Ok(codemodel) 55 | } 56 | } 57 | 58 | impl Codemodel { 59 | /// Turn this into [`configurations`](Self::configurations). 60 | pub fn into_conf(self) -> Vec { 61 | self.configurations 62 | } 63 | 64 | /// Turn this into [`configurations[0]`](Self::configurations). 65 | /// 66 | /// This functions panics if there are no configurations. 67 | pub fn into_first_conf(self) -> Configuration { 68 | self.configurations 69 | .into_iter() 70 | .next() 71 | .expect("no configurations") 72 | } 73 | 74 | /// The path to the directory containing the file represented by this 75 | /// [`Codemodel`] instance. 76 | pub fn dir_path(&self) -> &PathBuf { 77 | &self.codemodel_dir 78 | } 79 | } 80 | 81 | /// Paths used by cmake. 82 | #[derive(Debug, Deserialize, Clone)] 83 | pub struct Paths { 84 | /// The absolute path to the top-level source directory. 85 | pub source: PathBuf, 86 | /// The absolute path to the top-level build directory. 87 | pub build: PathBuf, 88 | } 89 | 90 | /// A build configuration. 91 | #[derive(Debug, Deserialize, Clone)] 92 | pub struct Configuration { 93 | #[serde(skip)] 94 | codemodel_dir: Arc, 95 | /// The name of the configuration (e.g. `Debug`) 96 | pub name: String, 97 | /// A build system target. 98 | /// 99 | /// Such targets are created by calls to `add_executable()`, `add_library()`, and 100 | /// `add_custom_target()`, excluding imported targets and interface libraries (which do 101 | /// not generate any build rules). 102 | #[serde(rename = "targets")] 103 | pub target_refs: Vec, 104 | } 105 | 106 | impl Configuration { 107 | /// Load a codemodel target object by name. 108 | pub fn get_target(&self, name: impl AsRef) -> Option> { 109 | self.target_refs 110 | .iter() 111 | .find(|t| t.name == name.as_ref()) 112 | .map(|t| t.load(self)) 113 | } 114 | 115 | /// Load all codemodel target objects. 116 | pub fn targets(&self) -> impl Iterator> + '_ { 117 | self.target_refs.iter().map(move |t| t.load(self)) 118 | } 119 | } 120 | 121 | /// A reference to a codemodel target object JSON file. 122 | #[derive(Debug, Deserialize, Clone)] 123 | #[serde(rename_all = "camelCase")] 124 | pub struct TargetRef { 125 | /// The target name. 126 | pub name: String, 127 | /// An unsigned integer 0-based index into the main directories array indicating the 128 | /// build system directory in which the target is defined. 129 | pub directory_index: usize, 130 | /// An unsigned integer 0-based index into the main projects array indicating the 131 | /// build system project in which the target is defined. 132 | pub project_index: usize, 133 | /// A path relative to the codemodel file to another JSON file containing a 134 | /// codemodel `target` object. 135 | pub json_file: String, 136 | } 137 | 138 | impl TargetRef { 139 | /// Load the target object from the [`json_file`](Self::json_file). 140 | pub fn load(&self, cfg: &Configuration) -> Result { 141 | target::Target::from_file(cfg.codemodel_dir.join(&self.json_file)) 142 | } 143 | } 144 | 145 | /// A cmake supported programming language 146 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] 147 | pub enum Language { 148 | C, 149 | #[serde(rename = "CXX")] 150 | Cpp, 151 | #[serde(rename = "CUDA")] 152 | Cuda, 153 | #[serde(rename = "OBJCXX")] 154 | ObjectiveCpp, 155 | #[serde(rename = "HIP")] 156 | Hip, 157 | #[serde(rename = "ISPC")] 158 | Ispc, 159 | #[serde(rename = "ASM")] 160 | Assembly, 161 | } 162 | 163 | pub use target::Target; 164 | 165 | /// Codemodel target cmake file API object. 166 | pub mod target { 167 | use std::path::{Path, PathBuf}; 168 | 169 | use anyhow::{anyhow, Context, Result}; 170 | use serde::Deserialize; 171 | 172 | use super::Language; 173 | 174 | /// A type of cmake target. 175 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Hash)] 176 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 177 | pub enum Type { 178 | Executable, 179 | StaticLibrary, 180 | SharedLibrary, 181 | ModuleLibrary, 182 | ObjectLibrary, 183 | InterfaceLibrary, 184 | Utility, 185 | } 186 | 187 | #[derive(Debug, Deserialize, Clone)] 188 | #[serde(rename_all = "camelCase")] 189 | pub struct Target { 190 | /// The logical name of the target. 191 | pub name: String, 192 | /// Link info of target. 193 | pub link: Option, 194 | /// Compile settings for source files. 195 | #[serde(default)] 196 | pub compile_groups: Vec, 197 | /// The type of the target. 198 | #[serde(rename = "type")] 199 | pub target_type: Type, 200 | } 201 | 202 | impl Target { 203 | /// Deserialize the codemodel target object JSON file from `file_path`. 204 | pub fn from_file(file_path: impl AsRef) -> Result { 205 | let file = std::fs::File::open(&file_path)?; 206 | let value: Target = serde_json::from_reader(file).with_context(|| { 207 | anyhow!( 208 | "Failed to parse the cmake-file-api target file '{}'", 209 | file_path.as_ref().display() 210 | ) 211 | })?; 212 | 213 | Ok(value) 214 | } 215 | } 216 | 217 | /// Compile settings for groups of sources using the same settings. 218 | #[derive(Debug, Deserialize, Clone)] 219 | #[serde(rename_all = "camelCase")] 220 | pub struct CompileGroup { 221 | /// Language of the toolchain in use to compile the source file. 222 | pub language: Language, 223 | /// Fragments of the compiler command line invocation if available. 224 | #[serde(default)] 225 | pub compile_command_fragments: Vec, 226 | /// Include directories. 227 | #[serde(default)] 228 | pub includes: Vec, 229 | /// Prerocessor definitions. 230 | #[serde(default)] 231 | pub defines: Vec, 232 | /// Path to the sysroot. 233 | /// 234 | /// Present when the `CMAKE_SYSROOT_COMPILE` or `CMAKE_SYSROOT` variable is defined. 235 | pub sysroot: Option, 236 | } 237 | 238 | /// A fragment of a compile command invocation. 239 | #[derive(Debug, Deserialize, Clone)] 240 | pub struct Fragment { 241 | /// A fragment of the compile command line invocation. 242 | /// 243 | /// The value is encoded in the build system's native shell format. 244 | pub fragment: String, 245 | } 246 | 247 | /// A preprocessor definition. 248 | #[derive(Debug, Deserialize, Clone)] 249 | pub struct Define { 250 | /// The preprocessor definition in the format `[=]`, e.g. `DEF` or `DEF=1`. 251 | pub define: String, 252 | } 253 | 254 | /// A include directory. 255 | #[derive(Debug, Deserialize, Clone)] 256 | #[serde(rename_all = "camelCase")] 257 | pub struct Include { 258 | /// The path to the include directory, represented with forward slashes. 259 | pub path: String, 260 | /// Whether the include directory is marked as a system include directory. 261 | #[serde(default)] 262 | pub is_system: bool, 263 | } 264 | 265 | /// Executable or shared library link information. 266 | #[derive(Debug, Deserialize, Clone)] 267 | #[serde(rename_all = "camelCase")] 268 | pub struct Link { 269 | /// The language of the toolchain that is used to invoke the linker. 270 | pub language: Language, 271 | /// Fragments of the link command line invocation. 272 | pub command_fragments: Vec, 273 | /// Whether link-time optimization is enabled. 274 | #[serde(default)] 275 | pub lto: bool, 276 | /// Path to the sysroot. 277 | /// 278 | /// Present when the `CMAKE_SYSROOT_LINK` or `CMAKE_SYSROOT` variable is defined. 279 | pub sysroot: Option, 280 | } 281 | 282 | /// A link command linke fragment. 283 | #[derive(Debug, Deserialize, Clone)] 284 | pub struct CommandFragment { 285 | /// A fragment of the link command line invocation. 286 | /// 287 | /// The value is encoded in the build system's native shell format. 288 | pub fragment: String, 289 | /// The role of the fragments content. 290 | pub role: Role, 291 | } 292 | 293 | #[derive(Debug, PartialEq, Eq, Deserialize, Clone, Copy)] 294 | #[serde(rename_all = "camelCase")] 295 | pub enum Role { 296 | /// Link flags 297 | Flags, 298 | /// Link library file paths or flags 299 | Libraries, 300 | /// Library search path flags 301 | LibraryPath, 302 | /// MacOS framework search path flags 303 | FrameworkPath, 304 | } 305 | 306 | /// Path to the sysroot. 307 | #[derive(Debug, Deserialize, Clone)] 308 | pub struct Sysroot { 309 | /// The absolute path to the sysroot, represented with forward slashes. 310 | pub path: PathBuf, 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /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 [yyyy] [name of copyright owner] 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. -------------------------------------------------------------------------------- /cargo-pio/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 [yyyy] [name of copyright owner] 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. -------------------------------------------------------------------------------- /ldproxy/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 [yyyy] [name of copyright owner] 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. -------------------------------------------------------------------------------- /src/bindgen.rs: -------------------------------------------------------------------------------- 1 | //! Bindgen utilities for generating bindings to C/C++ code. 2 | 3 | use std::ffi::OsStr; 4 | use std::path::{Path, PathBuf}; 5 | use std::{env, fs}; 6 | 7 | use anyhow::{anyhow, bail, Context, Error, Result}; 8 | 9 | use crate::cargo::out_dir; 10 | use crate::utils::OsStrExt; 11 | use crate::{cargo, cmd}; 12 | 13 | /// Re-export the bindgen crate so that downstream crates can use it without 14 | /// having to add it as a dependency. 15 | pub mod types { 16 | pub use ::bindgen::*; 17 | } 18 | 19 | /// The environment variable name containing the file path of the file that contains the 20 | /// generated bindings. 21 | pub const VAR_BINDINGS_FILE: &str = "EMBUILD_GENERATED_BINDINGS_FILE"; 22 | 23 | /// A builder for creating a [`bindgen::Builder`]. 24 | #[derive(Clone, Default, Debug)] 25 | #[must_use] 26 | pub struct Factory { 27 | pub clang_args: Vec, 28 | pub linker: Option, 29 | pub mcu: Option, 30 | pub force_cpp: bool, 31 | pub sysroot: Option, 32 | } 33 | 34 | impl Factory { 35 | /// Create a new factory populating the clang args, linker and mcu from the 36 | /// Scons variables of a platformio project. 37 | #[cfg(feature = "pio")] 38 | pub fn from_scons_vars(scons_vars: &crate::pio::project::SconsVariables) -> Result { 39 | use crate::cli; 40 | let clang_args = cli::NativeCommandArgs::new(&scons_vars.incflags) 41 | .chain(cli::NativeCommandArgs::new( 42 | scons_vars.clangargs.as_deref().unwrap_or_default(), 43 | )) 44 | .collect(); 45 | 46 | Ok(Self { 47 | clang_args, 48 | linker: Some(scons_vars.full_path(scons_vars.link.clone())?), 49 | mcu: scons_vars.mcu.clone(), 50 | force_cpp: false, 51 | sysroot: None, 52 | }) 53 | } 54 | 55 | /// Create a new factory populating the clang args, force cpp, and sysroot from the 56 | /// cmake file-api compile group. 57 | #[cfg(feature = "cmake")] 58 | pub fn from_cmake( 59 | compile_group: &crate::cmake::file_api::codemodel::target::CompileGroup, 60 | ) -> Result { 61 | use crate::cmake::file_api::codemodel::Language; 62 | assert!( 63 | compile_group.language == Language::C || compile_group.language == Language::Cpp, 64 | "Generating bindings for languages other than C/C++ is not supported" 65 | ); 66 | 67 | let clang_args = compile_group 68 | .defines 69 | .iter() 70 | .map(|d| format!("-D{}", d.define)) 71 | .chain( 72 | compile_group 73 | .includes 74 | .iter() 75 | .map(|i| format!("-I{}", &i.path)), 76 | ) 77 | .collect(); 78 | 79 | Ok(Self { 80 | clang_args, 81 | linker: None, 82 | force_cpp: compile_group.language == Language::Cpp, 83 | mcu: None, 84 | sysroot: compile_group.sysroot.as_ref().map(|s| s.path.clone()), 85 | }) 86 | } 87 | 88 | pub fn new() -> Self { 89 | Default::default() 90 | } 91 | 92 | /// Set the clang args that need to be passed down to the Bindgen instance. 93 | pub fn with_clang_args(mut self, clang_args: impl IntoIterator) -> Self 94 | where 95 | S: Into, 96 | { 97 | self.clang_args 98 | .extend(clang_args.into_iter().map(Into::into)); 99 | self 100 | } 101 | 102 | /// Set the sysroot to be used for generating bindings. 103 | pub fn with_sysroot(mut self, sysroot: impl Into) -> Self { 104 | self.sysroot = Some(sysroot.into()); 105 | self 106 | } 107 | 108 | /// Set the linker used to determine the sysroot to be used for generating bindings, if the sysroot is not explicitly passed. 109 | pub fn with_linker(mut self, linker: impl Into) -> Self { 110 | self.linker = Some(linker.into()); 111 | self 112 | } 113 | 114 | /// Create a [`bindgen::Builder`] with these settings. 115 | pub fn builder(self) -> Result { 116 | self.create_builder(false) 117 | } 118 | 119 | /// Create a [`bindgen::Builder`] creating C++ bindings with these settings. 120 | pub fn cpp_builder(self) -> Result { 121 | self.create_builder(true) 122 | } 123 | 124 | fn create_builder(self, cpp: bool) -> Result { 125 | let cpp = self.force_cpp || cpp; 126 | let sysroot = self 127 | .sysroot 128 | .clone() 129 | .map_or_else(|| try_get_sysroot(&self.linker), Ok)?; 130 | 131 | let sysroot_args = [ 132 | format!("--sysroot={}", sysroot.try_to_str()?), 133 | format!("-I{}", sysroot.join("include").try_to_str()?), 134 | ]; 135 | 136 | let cpp_args = if cpp { 137 | get_cpp_includes(&sysroot)? 138 | } else { 139 | vec![] 140 | }; 141 | 142 | let builder = bindgen::Builder::default() 143 | .use_core() 144 | .layout_tests(false) 145 | .formatter(bindgen::Formatter::None) 146 | .derive_default(true) 147 | .clang_arg("-D__bindgen") 148 | // Include directories provided by the build system 149 | // should be first on the search path (before sysroot includes), 150 | // or else libc's does not correctly override sysroot's 151 | .clang_args(&self.clang_args) 152 | .clang_args(sysroot_args) 153 | .clang_args(&["-x", if cpp { "c++" } else { "c" }]) 154 | .clang_args(cpp_args); 155 | 156 | log::debug!( 157 | "Bindgen builder factory flags: {:?}", 158 | builder.command_line_flags() 159 | ); 160 | 161 | Ok(builder) 162 | } 163 | } 164 | 165 | /// Get the default filename for bindings and set the environment variable named 166 | /// [`VAR_BINDINGS_FILE`] that is available during crate compilation to that path. 167 | pub fn default_bindings_file() -> Result { 168 | let bindings_file = out_dir().join("bindings.rs"); 169 | cargo::set_rustc_env(VAR_BINDINGS_FILE, bindings_file.try_to_str()?); 170 | Ok(bindings_file) 171 | } 172 | 173 | /// Create rust bindings in [`default_bindings_file`] using [`run_for_file`]. 174 | pub fn run(builder: bindgen::Builder) -> Result { 175 | let output_file = default_bindings_file()?; 176 | run_for_file(builder, &output_file)?; 177 | Ok(output_file) 178 | } 179 | 180 | /// Try to `cargo fmt` `file` using any of the current, `stable` and `nightly` toolchains. 181 | /// If all of them fail print a warning ([`cargo::print_warning`]). 182 | pub fn cargo_fmt_file(file: impl AsRef) { 183 | let file = file.as_ref(); 184 | // Run rustfmt on the generated bindings separately, because custom toolchains often do not have rustfmt 185 | // We try multiple rustfmt instances: 186 | // - The one from the currently active toolchain 187 | // - The one from stable 188 | // - The one from nightly 189 | if cmd!("rustfmt", file).run().is_err() 190 | && cmd!("rustup", "run", "stable", "rustfmt", file) 191 | .run() 192 | .is_err() 193 | && cmd!("rustup", "run", "nightly", "rustfmt", file) 194 | .run() 195 | .is_err() 196 | { 197 | cargo::print_warning( 198 | "rustfmt not found in the current toolchain, nor in stable or nightly. \ 199 | The generated bindings will not be properly formatted.", 200 | ); 201 | } 202 | } 203 | 204 | /// Create rust bindings in `output_file` and run `cargo fmt` over that file. 205 | pub fn run_for_file(builder: bindgen::Builder, output_file: impl AsRef) -> Result<()> { 206 | let output_file = output_file.as_ref(); 207 | 208 | eprintln!("Output: {output_file:?}"); 209 | eprintln!("Bindgen builder flags: {:?}", builder.command_line_flags()); 210 | 211 | let bindings = builder 212 | .generate() 213 | .map_err(|_| Error::msg("Failed to generate bindings"))?; 214 | 215 | bindings.write_to_file(output_file)?; 216 | cargo_fmt_file(output_file); 217 | 218 | Ok(()) 219 | } 220 | 221 | /// Extension trait for [`bindgen::Builder`]. 222 | /// Extension trait for [`bindgen::Builder`]. 223 | pub trait BindgenExt: Sized { 224 | /// Add all input C/C++ headers using repeated [`bindgen::Builder::header`]. 225 | fn path_headers(self, headers: impl IntoIterator>) -> Result; 226 | } 227 | 228 | impl BindgenExt for bindgen::Builder { 229 | fn path_headers(mut self, headers: impl IntoIterator>) -> Result { 230 | for header in headers { 231 | self = self.header(header.as_ref().try_to_str()?) 232 | } 233 | Ok(self) 234 | } 235 | } 236 | 237 | fn try_get_sysroot(linker: &Option>) -> Result { 238 | let linker = if let Some(ref linker) = linker { 239 | linker.as_ref().to_owned() 240 | } else if let Some(linker) = env::var_os("RUSTC_LINKER") { 241 | PathBuf::from(linker) 242 | } else { 243 | bail!("Could not determine linker: No explicit linker and `RUSTC_LINKER` not set"); 244 | }; 245 | 246 | let gcc_file_stem = linker 247 | .file_stem() 248 | .and_then(OsStr::to_str) 249 | .filter(|&s| s == "gcc" || s.ends_with("-gcc")); 250 | 251 | // For whatever reason, --print-sysroot does not work with GCC 252 | // Change it to LD 253 | let linker = if let Some(stem) = gcc_file_stem { 254 | let mut ld_linker = 255 | linker.with_file_name(format!("{}{}", stem.strip_suffix("gcc").unwrap(), "ld")); 256 | if let Some(ext) = linker.extension() { 257 | ld_linker.set_extension(ext); 258 | } 259 | ld_linker 260 | } else { 261 | linker 262 | }; 263 | 264 | cmd!(&linker, "--print-sysroot") 265 | .stdout() 266 | .with_context(|| { 267 | anyhow!( 268 | "Could not determine sysroot from linker '{}'", 269 | linker.display() 270 | ) 271 | }) 272 | .map(PathBuf::from) 273 | } 274 | 275 | fn get_cpp_includes(sysroot: impl AsRef) -> Result> { 276 | let sysroot = sysroot.as_ref(); 277 | let cpp_includes_root = sysroot.join("include").join("c++"); 278 | 279 | let cpp_version = fs::read_dir(cpp_includes_root)? 280 | .map(|dir_entry_r| dir_entry_r.map(|dir_entry| dir_entry.path())) 281 | .fold(None, |ao: Option, sr: Result| { 282 | if let Some(a) = ao.as_ref() { 283 | sr.ok() 284 | .map_or(ao.clone(), |s| if a >= &s { ao.clone() } else { Some(s) }) 285 | } else { 286 | sr.ok() 287 | } 288 | }); 289 | 290 | if let Some(cpp_version) = cpp_version { 291 | let mut cpp_include_paths = vec![ 292 | format!("-I{}", cpp_version.try_to_str()?), 293 | format!("-I{}", cpp_version.join("backward").try_to_str()?), 294 | ]; 295 | 296 | if let Some(sysroot_last_segment) = fs::canonicalize(sysroot)?.file_name() { 297 | cpp_include_paths.push(format!( 298 | "-I{}", 299 | cpp_version.join(sysroot_last_segment).try_to_str()? 300 | )); 301 | } 302 | 303 | Ok(cpp_include_paths) 304 | } else { 305 | Ok(Vec::new()) 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /src/cli/arg.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use bitflags::bitflags; 4 | 5 | /// The type of a command line argument. 6 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 7 | pub enum Arg { 8 | /// A flag with a name (ex. `-n` or `--name`). 9 | Flag, 10 | /// An option with a `name` and `value` 11 | /// 12 | /// Could be parsed depending on [`ArgOpts`] as: 13 | /// - `--name=value` 14 | /// - `--name value` 15 | /// - `--namevalue` 16 | /// - `-nvalue` 17 | /// - `-namevalue` 18 | /// - `-name value` 19 | /// - `-name=value` 20 | /// 21 | /// Is serialized as (default): 22 | /// - `-n value` if `name` is a single character, 23 | /// - `--name value` otherwise. 24 | Option, 25 | } 26 | 27 | impl Arg { 28 | /// Create an [`ArgDef`] from a `Arg::Flag` with `name`. 29 | pub const fn flag(name: &str) -> ArgDef<'_, 'static> { 30 | Self::Flag.with_name(name) 31 | } 32 | 33 | /// Create an [`ArgDef`] from an `Arg::Option` with `name`. 34 | pub const fn option(name: &str) -> ArgDef<'_, 'static> { 35 | Self::Option.with_name(name) 36 | } 37 | 38 | /// Create an [`ArgDef`] from this `Arg` with `name`. 39 | pub const fn with_name(self, name: &str) -> ArgDef<'_, 'static> { 40 | ArgDef { 41 | arg: self, 42 | name, 43 | alias: &[], 44 | opts: ArgOpts::empty(), 45 | } 46 | } 47 | } 48 | 49 | bitflags! { 50 | /// Argument options for parsing and formatting arguments. 51 | /// 52 | /// See [`ArgDef::parse`] and [`ArgDef::format`] for parsing and formatting. 53 | pub struct ArgOpts: u32 { 54 | /// The argument can use a single hypen (ex. `-`) 55 | const SINGLE_HYPHEN = (1 << 0); 56 | /// The argument can use two hyphens (ex. `--`) 57 | const DOUBLE_HYPHEN = (1 << 1); 58 | /// The argument can have whitespace to seperate the value (ex. `-- `) 59 | const VALUE_SEP_NEXT_ARG = (1 << 2); 60 | /// The argument can have an equals (`=`) to seperate the value (ex. 61 | /// `--=`) 62 | const VALUE_SEP_EQUALS = (1 << 3); 63 | /// The argument can have no seperator for the value (ex. `--`) 64 | /// 65 | /// Note: This will also match [`VALUE_SEP_EQUALS`](ArgOpts::VALUE_SEP_EQUALS) but 66 | /// keep the equals sign in the value: `--=` -> `Some("=")`. 67 | const VALUE_SEP_NO_SPACE = (1 << 4); 68 | /// The argument's value is optional 69 | const VALUE_OPTIONAL = (1 << 5); 70 | 71 | const ALL_HYPHEN = Self::SINGLE_HYPHEN.bits() | Self::DOUBLE_HYPHEN.bits(); 72 | const ALL_VALUE_SEP = Self::VALUE_SEP_EQUALS.bits() | Self::VALUE_SEP_NEXT_ARG.bits() | Self::VALUE_SEP_NO_SPACE.bits(); 73 | } 74 | } 75 | 76 | impl ArgOpts { 77 | /// Whether the options specify the support for `count` hyphens. 78 | /// 79 | /// If the options don't specify any support for hyphens assume all are supported. 80 | pub const fn is_hyphen_count(self, count: usize) -> bool { 81 | (count == 1 && self.contains(Self::SINGLE_HYPHEN)) 82 | || (count == 2 && self.contains(Self::DOUBLE_HYPHEN)) 83 | || ((count == 1 || count == 2) && !self.intersects(Self::ALL_HYPHEN)) 84 | } 85 | 86 | /// Whether the option value is optional. 87 | pub const fn is_value_optional(self) -> bool { 88 | self.contains(Self::VALUE_OPTIONAL) 89 | } 90 | 91 | /// Whether the beginning of `s` match any of the value seperator options specified. 92 | /// 93 | /// If one seperator option matches `out_sep_len` will be set to the char-length of 94 | /// the seperator or [`None`] if the value is supposed to be in the next argument. 95 | /// 96 | /// If no seperator options are set assumes all except [`ArgOpts::VALUE_SEP_NO_SPACE`]. 97 | pub(super) fn matches_value_sep(mut self, s: &str, out_sep_len: &mut Option) -> bool { 98 | if !self.intersects(ArgOpts::ALL_VALUE_SEP) { 99 | self |= ArgOpts::ALL_VALUE_SEP.difference(ArgOpts::VALUE_SEP_NO_SPACE); 100 | } 101 | 102 | let c = s.chars().next(); 103 | let (result, sep_len) = match c { 104 | Some('=') if self.contains(Self::VALUE_SEP_EQUALS) => (true, Some(1)), 105 | None if self.contains(Self::VALUE_SEP_NEXT_ARG) => (true, None), 106 | Some(_) if self.contains(Self::VALUE_SEP_NO_SPACE) => (true, Some(0)), 107 | None if self.contains(Self::VALUE_OPTIONAL) => (true, Some(0)), 108 | _ => (false, None), 109 | }; 110 | *out_sep_len = sep_len; 111 | result 112 | } 113 | } 114 | 115 | /// An command line argument definition of how to parse and format a specific argument. 116 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 117 | #[must_use] 118 | pub struct ArgDef<'s, 'a> { 119 | /// The type of argument (flag or option). 120 | pub arg: Arg, 121 | /// The name of the argument. 122 | pub name: &'s str, 123 | /// One or more alias and optionally corresponding [`ArgOpts`]. 124 | pub alias: &'a [(&'a str, Option)], 125 | /// The default [`ArgOpts`]. 126 | pub opts: ArgOpts, 127 | } 128 | 129 | impl<'s, 'a> ArgDef<'s, 'a> { 130 | /// Set the `alias`(es) for this definition, each alias can have their own [`ArgOpts`] 131 | /// which override the default [`opts`](ArgDef::opts) when set. 132 | pub const fn with_alias<'b>(self, alias: &'b [(&'b str, Option)]) -> ArgDef<'s, 'b> { 133 | ArgDef { 134 | alias, 135 | arg: self.arg, 136 | name: self.name, 137 | opts: self.opts, 138 | } 139 | } 140 | 141 | /// Set the options for this definition. 142 | pub const fn with_opts(mut self, opts: ArgOpts) -> ArgDef<'s, 'a> { 143 | self.opts = opts; 144 | self 145 | } 146 | 147 | /// Set as an argument requiring two `-`. 148 | pub const fn long(mut self) -> ArgDef<'s, 'a> { 149 | self.opts = self.opts.union(ArgOpts::DOUBLE_HYPHEN); 150 | self 151 | } 152 | 153 | /// Set as an argument requiring one `-`. 154 | pub const fn short(mut self) -> ArgDef<'s, 'a> { 155 | self.opts = self.opts.union(ArgOpts::SINGLE_HYPHEN); 156 | self 157 | } 158 | 159 | /// Iterate over the default and all aliases of this arg def. 160 | pub const fn iter(&self) -> ArgDefIter<'_> { 161 | ArgDefIter { 162 | alias_index: None, 163 | arg_def: self, 164 | } 165 | } 166 | 167 | /// Generate individual arguments from this argument definition and a `value`. 168 | /// 169 | /// `value` is ignored if this definition is of type [`Arg::Flag`]. 170 | /// 171 | /// The returned value can be iterated over to get all whitespace-separated parts of 172 | /// the argument, and it can be [`Display`]ed as a single string, where the parts will 173 | /// be separated by a whitespace. 174 | pub fn format(&self, value: Option<&str>) -> impl Iterator + Display { 175 | let ArgDef { 176 | arg, name, opts, .. 177 | } = *self; 178 | 179 | match arg { 180 | Arg::Flag if opts.is_empty() => { 181 | let second_hyphen = if self.name.len() > 1 { "-" } else { "" }; 182 | 183 | FormattedArg::One(format!("-{}{}", second_hyphen, self.name)) 184 | } 185 | Arg::Flag => { 186 | let second_hyphen = if opts.contains(ArgOpts::SINGLE_HYPHEN) { 187 | "" 188 | } else { 189 | "-" 190 | }; 191 | 192 | FormattedArg::One(format!("-{}{}", second_hyphen, self.name)) 193 | } 194 | Arg::Option => { 195 | assert!(value.is_some() || (value.is_none() && opts.is_value_optional())); 196 | 197 | let sep = if value.is_none() && opts.is_value_optional() { 198 | None 199 | } else if opts.contains(ArgOpts::VALUE_SEP_EQUALS) { 200 | Some("=") 201 | } else if opts.contains(ArgOpts::VALUE_SEP_NO_SPACE) { 202 | Some("") 203 | } else { 204 | None 205 | }; 206 | 207 | let second_hyphen = if opts.contains(ArgOpts::SINGLE_HYPHEN) { 208 | "" 209 | } else if opts.contains(ArgOpts::DOUBLE_HYPHEN) || name.len() > 1 { 210 | "-" 211 | } else { 212 | "" 213 | }; 214 | 215 | if let Some(sep) = sep { 216 | let f = format!("-{}{}{}{}", second_hyphen, name, sep, value.unwrap()); 217 | FormattedArg::One(f) 218 | } else { 219 | let f = format!("-{second_hyphen}{name}"); 220 | if let Some(value) = value { 221 | FormattedArg::Two(f, value.into()) 222 | } else { 223 | FormattedArg::One(f) 224 | } 225 | } 226 | } 227 | } 228 | } 229 | } 230 | 231 | /// An iterator that iterates over the default and all aliases of an [`ArgDef`]. 232 | pub struct ArgDefIter<'d> { 233 | arg_def: &'d ArgDef<'d, 'd>, 234 | alias_index: Option, 235 | } 236 | 237 | impl<'d> Iterator for ArgDefIter<'d> { 238 | type Item = (&'d str, ArgOpts); 239 | 240 | fn next(&mut self) -> Option { 241 | let ArgDefIter { 242 | arg_def, 243 | alias_index, 244 | } = self; 245 | 246 | if let Some(i) = alias_index { 247 | if *i >= arg_def.alias.len() { 248 | None 249 | } else { 250 | let (name, opts) = arg_def.alias[*i]; 251 | *i += 1; 252 | 253 | Some((name, opts.unwrap_or(arg_def.opts))) 254 | } 255 | } else { 256 | *alias_index = Some(0); 257 | Some((arg_def.name, arg_def.opts)) 258 | } 259 | } 260 | } 261 | 262 | #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 263 | enum FormattedArg { 264 | None, 265 | One(String), 266 | Two(String, String), 267 | } 268 | 269 | impl Iterator for FormattedArg { 270 | type Item = String; 271 | 272 | fn next(&mut self) -> Option { 273 | match self { 274 | Self::Two(first, second) => { 275 | let first = std::mem::take(first); 276 | let second = std::mem::take(second); 277 | *self = Self::One(second); 278 | Some(first) 279 | } 280 | Self::One(first) => { 281 | let first = std::mem::take(first); 282 | *self = Self::None; 283 | Some(first) 284 | } 285 | _ => None, 286 | } 287 | } 288 | } 289 | 290 | impl Display for FormattedArg { 291 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 292 | match self { 293 | Self::Two(first, second) => write!(f, "{first} {second}"), 294 | Self::One(first) => write!(f, "{first}"), 295 | Self::None => Ok(()), 296 | } 297 | } 298 | } 299 | 300 | #[cfg(test)] 301 | mod tests { 302 | use super::*; 303 | 304 | #[test] 305 | fn format_flag() { 306 | const DEF: ArgDef = Arg::flag("n"); 307 | const DEF_LONG: ArgDef = Arg::flag("name"); 308 | 309 | assert_eq!(&DEF.format(None).to_string(), "-n"); 310 | assert_eq!(&DEF.format(Some("hallo")).to_string(), "-n"); 311 | 312 | assert_eq!(&DEF_LONG.format(None).to_string(), "--name"); 313 | assert_eq!(&DEF_LONG.format(Some("hallo")).to_string(), "--name"); 314 | 315 | let def = Arg::flag("n").with_opts(ArgOpts::DOUBLE_HYPHEN); 316 | let long_def = Arg::flag("name").with_opts(ArgOpts::SINGLE_HYPHEN); 317 | 318 | assert_eq!(&def.format(None).to_string(), "--n"); 319 | assert_eq!(&def.format(Some("hallo")).to_string(), "--n"); 320 | assert_eq!(&long_def.format(None).to_string(), "-name"); 321 | assert_eq!(&long_def.format(Some("hallo")).to_string(), "-name"); 322 | } 323 | 324 | #[test] 325 | fn format_option() { 326 | const DEF: ArgDef = Arg::option("n"); 327 | const DEF_LONG: ArgDef = Arg::option("name"); 328 | 329 | assert_eq!(&DEF.format(Some("value")).to_string(), "-n value"); 330 | assert_eq!(&DEF_LONG.format(Some("value")).to_string(), "--name value"); 331 | 332 | const DEF1: ArgDef = Arg::option("n").with_opts(ArgOpts::DOUBLE_HYPHEN); 333 | const DEF1_LONG: ArgDef = Arg::option("name").with_opts(ArgOpts::SINGLE_HYPHEN); 334 | 335 | assert_eq!(&DEF1.format(Some("value")).to_string(), "--n value"); 336 | assert_eq!(&DEF1_LONG.format(Some("value")).to_string(), "-name value"); 337 | 338 | let def = Arg::option("n").with_opts(ArgOpts::VALUE_SEP_EQUALS); 339 | let def_long = Arg::option("name").with_opts(ArgOpts::VALUE_SEP_EQUALS); 340 | 341 | assert_eq!(&def.format(Some("value")).to_string(), "-n=value"); 342 | assert_eq!(&def_long.format(Some("value")).to_string(), "--name=value"); 343 | 344 | let def = Arg::option("n").with_opts(ArgOpts::VALUE_SEP_NO_SPACE); 345 | let def_long = Arg::option("name").with_opts(ArgOpts::VALUE_SEP_NO_SPACE); 346 | 347 | assert_eq!(&def.format(Some("value")).to_string(), "-nvalue"); 348 | assert_eq!(&def_long.format(Some("value")).to_string(), "--namevalue"); 349 | 350 | let def = Arg::option("n").with_opts(ArgOpts::VALUE_SEP_NEXT_ARG); 351 | let def_long = Arg::option("name").with_opts(ArgOpts::VALUE_SEP_NEXT_ARG); 352 | 353 | assert_eq!(&def.format(Some("value")).to_string(), "-n value"); 354 | assert_eq!(&def_long.format(Some("value")).to_string(), "--name value"); 355 | 356 | let def = 357 | Arg::option("name").with_opts(ArgOpts::SINGLE_HYPHEN | ArgOpts::VALUE_SEP_NEXT_ARG); 358 | let mut iter = def.format(Some("value")); 359 | 360 | assert_eq!(iter.next(), Some(String::from("-name"))); 361 | assert_eq!(iter.next(), Some(String::from("value"))); 362 | 363 | let def = Arg::option("name").with_opts( 364 | ArgOpts::DOUBLE_HYPHEN | ArgOpts::VALUE_SEP_NEXT_ARG | ArgOpts::VALUE_OPTIONAL, 365 | ); 366 | let mut iter = def.format(None); 367 | 368 | assert_eq!(iter.next(), Some(String::from("--name"))); 369 | assert_eq!(iter.next(), None); 370 | } 371 | } 372 | --------------------------------------------------------------------------------