├── tests ├── all │ ├── benches │ │ └── b1.rs │ ├── src │ │ ├── lib.rs │ │ ├── bin │ │ │ ├── otherbin.rs │ │ │ ├── reqfeat.rs │ │ │ └── reqfeat_slash.rs │ │ └── main.rs │ ├── tests │ │ └── t1.rs │ ├── bdep │ │ ├── src │ │ │ └── lib.rs │ │ └── Cargo.toml │ ├── devdep │ │ ├── src │ │ │ └── lib.rs │ │ └── Cargo.toml │ ├── examples │ │ └── ex1.rs │ ├── featdep │ │ ├── src │ │ │ └── lib.rs │ │ └── Cargo.toml │ ├── namedep │ │ ├── src │ │ │ └── lib.rs │ │ └── Cargo.toml │ ├── oldname │ │ ├── src │ │ │ └── lib.rs │ │ └── Cargo.toml │ ├── windep │ │ ├── src │ │ │ └── lib.rs │ │ └── Cargo.toml │ ├── path-dep │ │ ├── src │ │ │ └── lib.rs │ │ └── Cargo.toml │ ├── build.rs │ ├── bare-rust-version │ │ ├── src │ │ │ └── lib.rs │ │ └── Cargo.toml │ ├── Cargo.lock │ └── Cargo.toml ├── basic_workspace │ ├── ex_lib │ │ ├── src │ │ │ └── lib.rs │ │ └── Cargo.toml │ ├── src │ │ └── main.rs │ └── Cargo.toml ├── selftest.rs └── test_samples.rs ├── .gitignore ├── clippy.toml ├── README.md ├── Cargo.toml ├── LICENSE-MIT ├── .github └── workflows │ ├── main.yml │ └── release.yml ├── src ├── errors.rs ├── dependency.rs ├── diagnostic.rs ├── libtest.rs ├── messages.rs └── lib.rs └── CHANGELOG.md /tests/all/benches/b1.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/all/src/lib.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/all/tests/t1.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/all/bdep/src/lib.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/all/devdep/src/lib.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/all/examples/ex1.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/all/featdep/src/lib.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/all/namedep/src/lib.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/all/oldname/src/lib.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/all/windep/src/lib.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/all/path-dep/src/lib.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/all/build.rs: -------------------------------------------------------------------------------- 1 | fn main() {} 2 | -------------------------------------------------------------------------------- /tests/all/bare-rust-version/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/all/src/bin/otherbin.rs: -------------------------------------------------------------------------------- 1 | fn main() {} 2 | -------------------------------------------------------------------------------- /tests/all/src/bin/reqfeat.rs: -------------------------------------------------------------------------------- 1 | fn main() {} 2 | -------------------------------------------------------------------------------- /tests/basic_workspace/ex_lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/all/src/bin/reqfeat_slash.rs: -------------------------------------------------------------------------------- 1 | fn main() {} 2 | -------------------------------------------------------------------------------- /tests/basic_workspace/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() {} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | .idea/ 3 | target 4 | temp/ 5 | tmp/ 6 | -------------------------------------------------------------------------------- /tests/all/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | } 4 | -------------------------------------------------------------------------------- /tests/all/bdep/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bdep" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /tests/all/devdep/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "devdep" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /tests/all/windep/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "windep" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /tests/all/oldname/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "oldname" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /tests/all/path-dep/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "path-dep" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /tests/all/bare-rust-version/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bare-rust-version" 3 | version = "0.1.0" 4 | edition = "2021" 5 | rust-version = "1.60" 6 | 7 | [dependencies] 8 | -------------------------------------------------------------------------------- /tests/all/featdep/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "featdep" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [dependencies] 7 | 8 | [features] 9 | i128 = [] 10 | -------------------------------------------------------------------------------- /tests/all/namedep/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "namedep" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [dependencies] 7 | 8 | [lib] 9 | name = "different_name" 10 | -------------------------------------------------------------------------------- /tests/basic_workspace/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ex_bin" 3 | version = "0.1.0" 4 | 5 | [dependencies] 6 | ex_lib = { version = "0.1.0", path = "./ex_lib" } 7 | 8 | [workspace] 9 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | disallowed-types = [ 2 | { path = "std::collections::HashMap", reason = "HashMap's key order is unspecified and thus serializing such a type will result in a random output, use BTreeMap instead." } 3 | ] -------------------------------------------------------------------------------- /tests/basic_workspace/ex_lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ex_lib" 3 | version = "0.1.0" 4 | 5 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 6 | 7 | [dependencies] 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cargo_metadata 2 | 3 | Structured access to the output of `cargo metadata`. Usually used from within a `cargo-*` executable. 4 | 5 | Also supports serialization to aid in implementing `--message-format=json`-like 6 | output generation in `cargo-*` subcommands, since some of the types in what 7 | `cargo --message-format=json` emits are exactly the same as the ones from `cargo metadata`. 8 | 9 | [![Build Status](https://github.com/oli-obk/cargo_metadata/workflows/CI/badge.svg?branch=main)](https://github.com/oli-obk/cargo_metadata/actions/workflows/main.yml?query=branch%3Amain) 10 | [![crates.io](https://img.shields.io/crates/v/cargo_metadata.svg)](https://crates.io/crates/cargo_metadata) 11 | 12 | [Documentation](https://docs.rs/cargo_metadata/) 13 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo_metadata" 3 | version = "0.23.1" 4 | authors = ["Oliver Schneider "] 5 | repository = "https://github.com/oli-obk/cargo_metadata" 6 | description = "structured access to the output of `cargo metadata`" 7 | license = "MIT" 8 | readme = "README.md" 9 | edition = "2021" 10 | rust-version = "1.86.0" 11 | 12 | [dependencies] 13 | camino = { version = "1.1.10", features = ["serde1"] } 14 | cargo-platform = "0.3.0" 15 | derive_builder = { version = "0.20", optional = true } 16 | semver = { version = "1.0.26", features = ["serde"] } 17 | serde = { version = "1.0.219", features = ["derive"] } 18 | serde_json = { version = "1.0.142", features = ["unbounded_depth"] } 19 | thiserror = "2.0.12" 20 | 21 | [features] 22 | default = [] 23 | builder = ["derive_builder"] 24 | unstable = [] 25 | 26 | [package.metadata.docs.rs] 27 | all-features = true 28 | 29 | [package.metadata.cargo_metadata_test] 30 | some_field = true 31 | other_field = "foo" 32 | -------------------------------------------------------------------------------- /tests/all/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "all" 7 | version = "0.1.0" 8 | dependencies = [ 9 | "bdep", 10 | "bitflags", 11 | "devdep", 12 | "featdep", 13 | "namedep", 14 | "oldname", 15 | "path-dep", 16 | "windep", 17 | ] 18 | 19 | [[package]] 20 | name = "bdep" 21 | version = "0.1.0" 22 | 23 | [[package]] 24 | name = "bitflags" 25 | version = "1.0.4" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12" 28 | 29 | [[package]] 30 | name = "devdep" 31 | version = "0.1.0" 32 | 33 | [[package]] 34 | name = "featdep" 35 | version = "0.1.0" 36 | 37 | [[package]] 38 | name = "namedep" 39 | version = "0.1.0" 40 | 41 | [[package]] 42 | name = "oldname" 43 | version = "0.1.0" 44 | 45 | [[package]] 46 | name = "path-dep" 47 | version = "0.1.0" 48 | 49 | [[package]] 50 | name = "windep" 51 | version = "0.1.0" 52 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /tests/all/Cargo.toml: -------------------------------------------------------------------------------- 1 | # Used in tests/test_samples.rs 2 | [package] 3 | name = "all" 4 | version = "0.1.0" 5 | authors = ["Jane Doe "] 6 | edition = "2018" 7 | license = "MIT/Apache-2.0" 8 | license-file = "LICENSE" 9 | description = "Package description." 10 | categories = ["command-line-utilities"] 11 | keywords = ["cli"] 12 | readme = "README.md" 13 | repository = "https://github.com/oli-obk/cargo_metadata/" 14 | homepage = "https://github.com/oli-obk/cargo_metadata/" 15 | documentation = "https://docs.rs/cargo_metadata/" 16 | links = "foo" 17 | publish = false 18 | default-run = "otherbin" 19 | rust-version = "1.56" 20 | 21 | [package.metadata.docs.rs] 22 | all-features = true 23 | default-target = "x86_64-unknown-linux-gnu" 24 | rustc-args = [ "--example-rustc-arg" ] 25 | 26 | [dependencies] 27 | path-dep = { path = "path-dep" } 28 | namedep = { path = "namedep" } 29 | bitflags = { version = "1.0", optional = true } 30 | featdep = { path = "featdep", features = ["i128"], default-features = false } 31 | newname = { path = "oldname", package = "oldname" } 32 | 33 | [dev-dependencies] 34 | devdep = { path = "devdep" } 35 | 36 | [build-dependencies] 37 | bdep = { path = "bdep" } 38 | 39 | [target.'cfg(windows)'.dependencies] 40 | windep = { path = "windep" } 41 | 42 | [features] 43 | default = ["feat1", "bitflags"] 44 | feat1 = [] 45 | feat2 = [] 46 | 47 | [lib] 48 | crate-type = ["rlib", "cdylib", "staticlib"] 49 | 50 | [[bin]] 51 | name = "otherbin" 52 | edition = '2015' 53 | doc = false 54 | 55 | [[bin]] 56 | name = "reqfeat" 57 | required-features = ["feat2"] 58 | 59 | [[bin]] 60 | name = "reqfeat_slash" 61 | required-features = ["featdep/i128"] 62 | 63 | [workspace] 64 | exclude = ["bare-rust-version", "bdep", "benches", "devdep", "examples", "featdep", "namedep", "oldname", "path-dep", "windep"] 65 | 66 | [workspace.metadata.testobject] 67 | myvalue = "abc" 68 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | rustfmt: 6 | name: rustfmt 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Install rust 11 | run: rustup update --no-self-update stable && rustup default stable && rustup component add rustfmt --toolchain stable 12 | - name: Check formatting 13 | run: cargo fmt -- --check 14 | 15 | clippy: 16 | name: clippy 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Install rust 21 | run: rustup update --no-self-update stable && rustup default stable && rustup component add clippy --toolchain stable 22 | - name: Clippy check 23 | run: cargo clippy --all-features -- -Dwarnings 24 | 25 | test: 26 | name: Test 27 | runs-on: ubuntu-latest 28 | strategy: 29 | matrix: 30 | include: 31 | - rust: stable 32 | - rust: beta 33 | - rust: nightly 34 | - rust: 1.86.0 35 | steps: 36 | - uses: actions/checkout@v2 37 | - name: Install rust 38 | run: rustup update --no-self-update ${{ matrix.rust }} && rustup default ${{ matrix.rust }} 39 | - name: Run tests 40 | run: | 41 | cargo build --verbose 42 | cargo build --verbose --no-default-features 43 | cargo test --verbose 44 | cargo test --verbose --no-default-features 45 | 46 | # Test with all features. But do not use the `unstable` feature on non-nightly toolchains 47 | if [ "${{ matrix.rust }}" = "nightly" ]; then 48 | cargo test --verbose --all-features 49 | else 50 | ALL_FEATURES_EXCEPT_UNSTABLE=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].features | keys | .[]' | grep -v 'unstable' | tr '\n' ' ') 51 | cargo test --features "$ALL_FEATURES_EXCEPT_UNSTABLE" 52 | fi 53 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use std::{io, str::Utf8Error, string::FromUtf8Error}; 2 | 3 | /// Custom result type for `cargo_metadata::Error` 4 | pub type Result = ::std::result::Result; 5 | 6 | /// Error returned when executing/parsing `cargo metadata` fails. 7 | /// 8 | /// # Note about Backtraces 9 | /// 10 | /// This error type does not contain backtraces, but each error variant 11 | /// comes from _one_ specific place, so it's not really needed for the 12 | /// inside of this crate. If you need a backtrace down to, but not inside 13 | /// of, a failed call of `cargo_metadata` you can do one of multiple thinks: 14 | /// 15 | /// 1. Convert it to a `failure::Error` (possible using the `?` operator), 16 | /// which is similar to a `Box<::std::error::Error + 'static + Send + Sync>`. 17 | /// 2. Have appropriate variants in your own error type. E.g. you could wrap 18 | /// a `failure::Context` or add a `failure::Backtrace` field (which 19 | /// is empty if `RUST_BACKTRACE` is not set, so it's simple to use). 20 | /// 3. You still can place a failure based error into a `error_chain` if you 21 | /// really want to. (Either through foreign_links or by making it a field 22 | /// value of a `ErrorKind` variant). 23 | /// 24 | #[derive(Debug, thiserror::Error)] 25 | pub enum Error { 26 | /// Error during execution of `cargo metadata` 27 | #[error("`cargo metadata` exited with an error: {stderr}")] 28 | CargoMetadata { 29 | /// stderr returned by the `cargo metadata` command 30 | stderr: String, 31 | }, 32 | 33 | /// IO Error during execution of `cargo metadata` 34 | #[error("failed to start `cargo metadata`: {0}")] 35 | Io(#[from] io::Error), 36 | 37 | /// Output of `cargo metadata` was not valid utf8 38 | #[error("cannot convert the stdout of `cargo metadata`: {0}")] 39 | Utf8(#[from] Utf8Error), 40 | 41 | /// Error output of `cargo metadata` was not valid utf8 42 | #[error("cannot convert the stderr of `cargo metadata`: {0}")] 43 | ErrUtf8(#[from] FromUtf8Error), 44 | 45 | /// Deserialization error (structure of json did not match expected structure) 46 | #[error("failed to interpret `cargo metadata`'s json: {0}")] 47 | Json(#[from] ::serde_json::Error), 48 | 49 | /// The output did not contain any json 50 | #[error("could not find any json in the output of `cargo metadata`")] 51 | NoJson, 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release new version 2 | 3 | on: 4 | workflow_dispatch: 5 | secrets: 6 | CARGO_REGISTRY_TOKEN: 7 | required: true 8 | 9 | env: 10 | RUST_BACKTRACE: 1 11 | CARGO_TERM_COLOR: always 12 | 13 | jobs: 14 | create-release: 15 | name: Create release 16 | runs-on: ubuntu-latest 17 | if: github.ref == 'refs/heads/main' 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v3 21 | with: 22 | persist-credentials: true 23 | 24 | - name: Install rust 25 | uses: actions-rs/toolchain@v1 26 | with: 27 | toolchain: stable 28 | profile: minimal 29 | override: true 30 | 31 | - uses: Swatinem/rust-cache@v2 32 | 33 | # Determine which version we're about to publish, so we can tag it appropriately. 34 | # If the tag already exists, then we've already published this version. 35 | - name: Determine current version 36 | id: version-check 37 | run: | 38 | # Fail on first error, on undefined variables, and on errors in pipes. 39 | set -euo pipefail 40 | export VERSION="$(cargo metadata --format-version 1 | \ 41 | jq --arg crate_name cargo_metadata --exit-status -r \ 42 | '.packages[] | select(.name == $crate_name) | .version')" 43 | echo "version=$VERSION" >> $GITHUB_OUTPUT 44 | if [[ "$(git tag -l "$VERSION")" != '' ]]; then 45 | echo "Aborting: Version $VERSION is already published, we found its tag in the repo." 46 | exit 1 47 | fi 48 | 49 | - name: Semver-check 50 | uses: obi1kenobi/cargo-semver-checks-action@v2 51 | with: 52 | rust-toolchain: manual # we've already installed Rust, don't install a new one 53 | 54 | - name: Publish 55 | run: cargo publish 56 | env: 57 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 58 | 59 | - name: Tag the version 60 | run: | 61 | # Fail on first error, on undefined variables, and on errors in pipes. 62 | set -euo pipefail 63 | git tag "${{ steps.version-check.outputs.version }}" 64 | git push origin "${{ steps.version-check.outputs.version }}" 65 | 66 | - uses: taiki-e/create-gh-release-action@v1 67 | name: Create GitHub release 68 | with: 69 | branch: main 70 | ref: refs/tags/${{ steps.version-check.outputs.version }} 71 | env: 72 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | - n/a 6 | 7 | ### Added 8 | 9 | - n/a 10 | 11 | ### Changed 12 | 13 | - n/a 14 | 15 | ### Removed 16 | 17 | - n/a 18 | 19 | ### Fixed 20 | 21 | - n/a 22 | 23 | ## [0.23.1] - 2025-11-11 24 | 25 | ### Changed 26 | 27 | - Stabilized `build_directory` 28 | 29 | ## [0.23.0] - 2025-09-27 30 | 31 | ### Added 32 | 33 | - Added `FeatureName` and `PackageName` newtype wrappers. 34 | 35 | ## [0.22.0] - 2025-08-18 36 | 37 | ### Added 38 | 39 | - Added `pub fn env_remove>(&mut self, key: K) -> &mut MetadataCommand` to `MetadataCommand`. 40 | - Added export of `cargo_platform` at crate's root module. 41 | 42 | ### Changed 43 | 44 | - Updated dependencies: 45 | - `camino` from `1.0.7` to `1.1.10` 46 | - `cargo_platform` from `0.2.0` to `0.3.0` 47 | - `derive_builder` from `0.12` to `0.20` 48 | - `semver` from `1.0.7` to `1.0.26` 49 | - `serde_json` from `1.0.118` to `1.0.142` 50 | - `serde` from `1.0.136` to `1.0.219` 51 | - `thiserror` from `2.0.3` to `2.0.12` 52 | - Made `Dependency`'s `source` member the same type as `Package`'s `source` member: `Option`. 53 | 54 | ## [0.19.0] - 2024-11-20 55 | 56 | ### Added 57 | 58 | - Re-exported `semver` crate directly. 59 | - Added implementation of `std::ops::Index<&PackageId>` for `Resolve`. 60 | - Added `pub fn is_kind(&self, name: TargetKind) -> bool` to `Target`. 61 | - Added derived implementations of `PartialEq`, `Eq` and `Hash` for `Metadata` and its members' types. 62 | - Added default fields to `PackageBuilder`. 63 | - Added `pub fn new(name:version:id:path:) -> Self` to `PackageBuilder` for providing all required fields upfront. 64 | 65 | ### Changed 66 | 67 | - Bumped MSRV from `1.42.0` to `1.56.0`. 68 | - Made `parse_stream` more versatile by accepting anything that implements `Read`. 69 | - Converted `TargetKind` and `CrateType` to an enum representation. 70 | 71 | ### Removed 72 | 73 | - Removed re-exports for `BuildMetadata` and `Prerelease` from `semver` crate. 74 | - Removed `.is_lib(…)`, `.is_bin(…)`, `.is_example(…)`, `.is_test(…)`, `.is_bench(…)`, `.is_custom_build(…)`, and `.is_proc_macro(…)` from `Target` (in favor of adding `.is_kind(…)`). 75 | 76 | ### Fixed 77 | 78 | - Added missing `manifest_path` field to `Artifact`. Fixes #187. 79 | 80 | ## [0.15.0] - 2022-06-22 81 | 82 | ### Added 83 | 84 | - Re-exported `BuildMetadata` and `Prerelease` from `semver` crate. 85 | - Added `workspace_packages` function. 86 | - Added `Edition` enum to better parse edition field. 87 | - Added `rust-version` field to Cargo manifest. 88 | 89 | ### Changed 90 | 91 | - Bumped msrv from `1.40.0` to `1.42.0`. 92 | 93 | ### Internal Changes 94 | 95 | - Updated `derive_builder` to the latest version. 96 | - Made use of `matches!` macros where possible. 97 | - Fixed some tests 98 | 99 | ## [0.15.1] - 2022-10-13 100 | 101 | ### Added 102 | 103 | - Added `TestMessage`, `TestEvent`, `SuiteEvent` for parsing the `cargo test -- --format json` output. 104 | -------------------------------------------------------------------------------- /src/dependency.rs: -------------------------------------------------------------------------------- 1 | //! This module contains `Dependency` and the types/functions it uses for deserialization. 2 | 3 | use std::fmt; 4 | 5 | use camino::Utf8PathBuf; 6 | #[cfg(feature = "builder")] 7 | use derive_builder::Builder; 8 | use semver::VersionReq; 9 | use serde::{Deserialize, Deserializer, Serialize}; 10 | 11 | use crate::Source; 12 | 13 | #[derive(Eq, PartialEq, Clone, Debug, Copy, Hash, Serialize, Deserialize, Default)] 14 | /// Dependencies can come in three kinds 15 | pub enum DependencyKind { 16 | #[serde(rename = "normal")] 17 | #[default] 18 | /// The 'normal' kind 19 | Normal, 20 | #[serde(rename = "dev")] 21 | /// Those used in tests only 22 | Development, 23 | #[serde(rename = "build")] 24 | /// Those used in build scripts only 25 | Build, 26 | #[doc(hidden)] 27 | #[serde(other)] 28 | Unknown, 29 | } 30 | 31 | impl fmt::Display for DependencyKind { 32 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 33 | let s = serde_json::to_string(self).unwrap(); 34 | // skip opening and closing quotes 35 | f.write_str(&s[1..s.len() - 1]) 36 | } 37 | } 38 | 39 | /// The `kind` can be `null`, which is interpreted as the default - `Normal`. 40 | pub(super) fn parse_dependency_kind<'de, D>(d: D) -> Result 41 | where 42 | D: Deserializer<'de>, 43 | { 44 | Deserialize::deserialize(d).map(|x: Option<_>| x.unwrap_or_default()) 45 | } 46 | 47 | #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)] 48 | #[cfg_attr(feature = "builder", derive(Builder))] 49 | #[non_exhaustive] 50 | #[cfg_attr(feature = "builder", builder(pattern = "owned", setter(into)))] 51 | /// A dependency of the main crate 52 | pub struct Dependency { 53 | /// Name as given in the `Cargo.toml` 54 | pub name: String, 55 | /// The source of dependency 56 | pub source: Option, 57 | /// The required version 58 | pub req: VersionReq, 59 | /// The kind of dependency this is 60 | #[serde(deserialize_with = "parse_dependency_kind")] 61 | pub kind: DependencyKind, 62 | /// Whether this dependency is required or optional 63 | pub optional: bool, 64 | /// Whether the default features in this dependency are used. 65 | pub uses_default_features: bool, 66 | /// The list of features enabled for this dependency. 67 | pub features: Vec, 68 | /// The target this dependency is specific to. 69 | /// 70 | /// Use the [`Display`] trait to access the contents. 71 | /// 72 | /// [`Display`]: std::fmt::Display 73 | pub target: Option, 74 | /// If the dependency is renamed, this is the new name for the dependency 75 | /// as a string. None if it is not renamed. 76 | pub rename: Option, 77 | /// The URL of the index of the registry where this dependency is from. 78 | /// 79 | /// If None, the dependency is from crates.io. 80 | pub registry: Option, 81 | /// The file system path for a local path dependency. 82 | /// 83 | /// Only produced on cargo 1.51+ 84 | pub path: Option, 85 | } 86 | 87 | pub use cargo_platform::Platform; 88 | -------------------------------------------------------------------------------- /src/diagnostic.rs: -------------------------------------------------------------------------------- 1 | //! This module contains `Diagnostic` and the types/functions it uses for deserialization. 2 | 3 | #[cfg(feature = "builder")] 4 | use derive_builder::Builder; 5 | use serde::{Deserialize, Serialize}; 6 | use std::fmt; 7 | 8 | /// The error code associated to this diagnostic. 9 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] 10 | #[cfg_attr(feature = "builder", derive(Builder))] 11 | #[non_exhaustive] 12 | #[cfg_attr(feature = "builder", builder(pattern = "owned", setter(into)))] 13 | pub struct DiagnosticCode { 14 | /// The code itself. 15 | pub code: String, 16 | /// An explanation for the code 17 | pub explanation: Option, 18 | } 19 | 20 | /// A line of code associated with the Diagnostic 21 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] 22 | #[cfg_attr(feature = "builder", derive(Builder))] 23 | #[non_exhaustive] 24 | #[cfg_attr(feature = "builder", builder(pattern = "owned", setter(into)))] 25 | pub struct DiagnosticSpanLine { 26 | /// The line of code associated with the error 27 | pub text: String, 28 | /// Start of the section of the line to highlight. 1-based, character offset in self.text 29 | pub highlight_start: usize, 30 | /// End of the section of the line to highlight. 1-based, character offset in self.text 31 | pub highlight_end: usize, 32 | } 33 | 34 | /// Macro expansion information associated with a diagnostic. 35 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] 36 | #[cfg_attr(feature = "builder", derive(Builder))] 37 | #[non_exhaustive] 38 | #[cfg_attr(feature = "builder", builder(pattern = "owned", setter(into)))] 39 | pub struct DiagnosticSpanMacroExpansion { 40 | /// span where macro was applied to generate this code; note that 41 | /// this may itself derive from a macro (if 42 | /// `span.expansion.is_some()`) 43 | pub span: DiagnosticSpan, 44 | 45 | /// name of macro that was applied (e.g., "foo!" or "#[derive(Eq)]") 46 | pub macro_decl_name: String, 47 | 48 | /// span where macro was defined (if known) 49 | pub def_site_span: Option, 50 | } 51 | 52 | /// A section of the source code associated with a Diagnostic 53 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] 54 | #[cfg_attr(feature = "builder", derive(Builder))] 55 | #[non_exhaustive] 56 | #[cfg_attr(feature = "builder", builder(pattern = "owned", setter(into)))] 57 | pub struct DiagnosticSpan { 58 | /// The file name or the macro name this diagnostic comes from. 59 | pub file_name: String, 60 | /// The byte offset in the file where this diagnostic starts from. 61 | pub byte_start: u32, 62 | /// The byte offset in the file where this diagnostic ends. 63 | pub byte_end: u32, 64 | /// 1-based. The line in the file. 65 | pub line_start: usize, 66 | /// 1-based. The line in the file. 67 | pub line_end: usize, 68 | /// 1-based, character offset. 69 | pub column_start: usize, 70 | /// 1-based, character offset. 71 | pub column_end: usize, 72 | /// Is this a "primary" span -- meaning the point, or one of the points, 73 | /// where the error occurred? 74 | /// 75 | /// There are rare cases where multiple spans are marked as primary, 76 | /// e.g. "immutable borrow occurs here" and "mutable borrow ends here" can 77 | /// be two separate spans both "primary". Top (parent) messages should 78 | /// always have at least one primary span, unless it has 0 spans. Child 79 | /// messages may have 0 or more primary spans. 80 | pub is_primary: bool, 81 | /// Source text from the start of line_start to the end of line_end. 82 | pub text: Vec, 83 | /// Label that should be placed at this location (if any) 84 | pub label: Option, 85 | /// If we are suggesting a replacement, this will contain text 86 | /// that should be sliced in atop this span. 87 | pub suggested_replacement: Option, 88 | /// If the suggestion is approximate 89 | pub suggestion_applicability: Option, 90 | /// Macro invocations that created the code at this span, if any. 91 | pub expansion: Option>, 92 | } 93 | 94 | /// Whether a suggestion can be safely applied. 95 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] 96 | #[non_exhaustive] 97 | pub enum Applicability { 98 | /// The suggested replacement can be applied automatically safely 99 | MachineApplicable, 100 | /// The suggested replacement has placeholders that will need to be manually 101 | /// replaced. 102 | HasPlaceholders, 103 | /// The suggested replacement may be incorrect in some circumstances. Needs 104 | /// human review. 105 | MaybeIncorrect, 106 | /// The suggested replacement will probably not work. 107 | Unspecified, 108 | } 109 | 110 | /// The diagnostic level 111 | #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] 112 | #[non_exhaustive] 113 | #[serde(rename_all = "lowercase")] 114 | pub enum DiagnosticLevel { 115 | /// Internal compiler error 116 | #[serde(rename = "error: internal compiler error")] 117 | Ice, 118 | /// Error 119 | Error, 120 | /// Warning 121 | Warning, 122 | /// Failure note 123 | #[serde(rename = "failure-note")] 124 | FailureNote, 125 | /// Note 126 | Note, 127 | /// Help 128 | Help, 129 | } 130 | 131 | /// A diagnostic message generated by rustc 132 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] 133 | #[cfg_attr(feature = "builder", derive(Builder))] 134 | #[non_exhaustive] 135 | #[cfg_attr(feature = "builder", builder(pattern = "owned", setter(into)))] 136 | pub struct Diagnostic { 137 | /// The error message of this diagnostic. 138 | pub message: String, 139 | /// The associated error code for this diagnostic 140 | pub code: Option, 141 | /// "error: internal compiler error", "error", "warning", "note", "help" 142 | pub level: DiagnosticLevel, 143 | /// A list of source code spans this diagnostic is associated with. 144 | pub spans: Vec, 145 | /// Associated diagnostic messages. 146 | pub children: Vec, 147 | /// The message as rustc would render it 148 | pub rendered: Option, 149 | } 150 | 151 | impl fmt::Display for Diagnostic { 152 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 153 | if let Some(ref rendered) = self.rendered { 154 | f.write_str(rendered)?; 155 | } else { 156 | f.write_str("cargo didn't render this message")?; 157 | } 158 | Ok(()) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/libtest.rs: -------------------------------------------------------------------------------- 1 | //! Parses output of [libtest](https://github.com/rust-lang/rust/blob/master/library/test/src/formatters/json.rs). 2 | //! 3 | //! Since this module parses output in an unstable format, all structs in this module may change at any time, and are exempt from semver guarantees. 4 | use serde::{Deserialize, Serialize}; 5 | 6 | /// Suite related event 7 | #[derive(Debug, PartialEq, Deserialize, Serialize)] 8 | #[serde(tag = "event")] 9 | #[serde(rename_all = "lowercase")] 10 | /// Suite event 11 | pub enum SuiteEvent { 12 | /// emitted on the start of a test run, and the start of the doctests 13 | Started { 14 | /// number of tests in this suite 15 | test_count: usize, 16 | }, 17 | /// the suite has finished 18 | Ok { 19 | /// the number of tests that passed 20 | passed: usize, 21 | /// the number of tests that failed 22 | failed: usize, 23 | /// number of tests that were ignored 24 | ignored: usize, 25 | /// number of benchmarks run 26 | measured: usize, 27 | /// i think this is based on what you specify in the cargo test argument 28 | filtered_out: usize, 29 | /// how long the suite took to run 30 | exec_time: f32, 31 | }, 32 | /// the suite has at least one failing test 33 | Failed { 34 | /// the number of tests that passed 35 | passed: usize, 36 | /// the number of tests that failed 37 | failed: usize, 38 | /// number of tests that were ignored 39 | ignored: usize, 40 | /// i think its something to do with benchmarks? 41 | measured: usize, 42 | /// i think this is based on what you specify in the cargo test argument 43 | filtered_out: usize, 44 | /// how long the suite took to run 45 | exec_time: f32, 46 | }, 47 | } 48 | 49 | #[derive(Debug, PartialEq, Deserialize, Serialize)] 50 | #[serde(tag = "event")] 51 | #[serde(rename_all = "lowercase")] 52 | /// Test event 53 | pub enum TestEvent { 54 | /// a new test starts 55 | Started { 56 | /// the name of this test 57 | name: String, 58 | }, 59 | /// the test has finished 60 | Ok { 61 | /// which one 62 | name: String, 63 | /// in how long 64 | exec_time: f32, 65 | /// what did it say? 66 | stdout: Option, 67 | }, 68 | /// the test has failed 69 | Failed { 70 | /// which one 71 | name: String, 72 | /// in how long 73 | exec_time: f32, 74 | /// why? 75 | stdout: Option, 76 | /// it timed out? 77 | reason: Option, 78 | /// what message 79 | message: Option, 80 | }, 81 | /// the test has been ignored 82 | Ignored { 83 | /// which one 84 | name: String, 85 | }, 86 | /// the test has timed out 87 | Timeout { 88 | /// which one 89 | name: String, 90 | }, 91 | } 92 | 93 | impl TestEvent { 94 | /// Get the name of this test 95 | pub fn name(&self) -> &str { 96 | let (Self::Started { name } 97 | | Self::Ok { name, .. } 98 | | Self::Ignored { name } 99 | | Self::Failed { name, .. } 100 | | Self::Timeout { name }) = self; 101 | name 102 | } 103 | 104 | /// Get the stdout of this test, if available. 105 | pub fn stdout(&self) -> Option<&str> { 106 | match self { 107 | Self::Ok { stdout, .. } | Self::Failed { stdout, .. } => stdout.as_deref(), 108 | _ => None, 109 | } 110 | } 111 | } 112 | 113 | #[derive(Debug, PartialEq, Deserialize, Serialize)] 114 | /// Represents the output of `cargo test -- -Zunstable-options --report-time --show-output --format json`. 115 | /// 116 | /// requires --report-time 117 | /// 118 | /// # Stability 119 | /// 120 | /// As this struct is for interfacing with the unstable libtest json output, this struct may change at any time, without semver guarantees. 121 | #[serde(tag = "type")] 122 | #[serde(rename_all = "lowercase")] 123 | pub enum TestMessage { 124 | /// suite related message 125 | Suite(SuiteEvent), 126 | /// test related message 127 | Test(TestEvent), 128 | /// bench related message 129 | Bench { 130 | /// name of benchmark 131 | name: String, 132 | /// distribution 133 | median: f32, 134 | /// deviation 135 | deviation: f32, 136 | /// thruput in MiB per second 137 | mib_per_second: Option, 138 | }, 139 | } 140 | 141 | #[test] 142 | fn deser() { 143 | macro_rules! run { 144 | ($($input:literal parses to $output:expr),+) => { 145 | $(assert_eq!(dbg!(serde_json::from_str::($input)).unwrap(), $output);)+ 146 | }; 147 | } 148 | run![ 149 | r#"{ "type": "suite", "event": "started", "test_count": 2 }"# parses to TestMessage::Suite(SuiteEvent::Started { test_count: 2 }), 150 | r#"{ "type": "test", "event": "started", "name": "fail" }"# parses to TestMessage::Test(TestEvent::Started { name: "fail".into() }), 151 | r#"{ "type": "test", "name": "fail", "event": "ok", "exec_time": 0.000003428, "stdout": "hello world" }"# parses to TestMessage::Test(TestEvent::Ok { name: "fail".into(), exec_time: 0.000003428, stdout: Some("hello world".into()) }), 152 | r#"{ "type": "test", "event": "started", "name": "nope" }"# parses to TestMessage::Test(TestEvent::Started { name: "nope".into() }), 153 | r#"{ "type": "test", "name": "nope", "event": "ignored" }"# parses to TestMessage::Test(TestEvent::Ignored { name: "nope".into() }), 154 | r#"{ "type": "suite", "event": "ok", "passed": 1, "failed": 0, "ignored": 1, "measured": 0, "filtered_out": 0, "exec_time": 0.000684028 }"# parses to TestMessage::Suite(SuiteEvent::Ok { passed: 1, failed: 0, ignored: 1, measured: 0, filtered_out: 0, exec_time: 0.000684028 }) 155 | ]; 156 | 157 | run![ 158 | r#"{ "type": "suite", "event": "started", "test_count": 2 }"# parses to TestMessage::Suite(SuiteEvent::Started { test_count: 2 }), 159 | r#"{ "type": "test", "event": "started", "name": "fail" }"# parses to TestMessage::Test(TestEvent::Started { name: "fail".into() }), 160 | r#"{ "type": "test", "event": "started", "name": "benc" }"# parses to TestMessage::Test(TestEvent::Started { name: "benc".into() }), 161 | r#"{ "type": "bench", "name": "benc", "median": 0, "deviation": 0 }"# parses to TestMessage::Bench { name: "benc".into(), median: 0., deviation: 0., mib_per_second: None }, 162 | r#"{ "type": "test", "name": "fail", "event": "failed", "exec_time": 0.000081092, "stdout": "thread 'fail' panicked" }"# parses to TestMessage::Test(TestEvent::Failed { name: "fail".into(), exec_time: 0.000081092, stdout: Some("thread 'fail' panicked".into()), reason: None, message: None} ), 163 | r#"{ "type": "suite", "event": "failed", "passed": 0, "failed": 1, "ignored": 0, "measured": 1, "filtered_out": 0, "exec_time": 0.000731068 }"# parses to TestMessage::Suite(SuiteEvent::Failed { passed: 0, failed: 1, ignored: 0, measured: 1, filtered_out: 0, exec_time: 0.000731068 }) 164 | ]; 165 | } 166 | -------------------------------------------------------------------------------- /tests/selftest.rs: -------------------------------------------------------------------------------- 1 | use std::env::current_dir; 2 | use std::path::PathBuf; 3 | 4 | use semver::Version; 5 | 6 | use cargo_metadata::{CargoOpt, Error, MetadataCommand}; 7 | use serde::Deserialize; 8 | 9 | #[derive(Debug, PartialEq, Eq, Deserialize)] 10 | struct TestPackageMetadata { 11 | some_field: bool, 12 | other_field: String, 13 | } 14 | 15 | #[test] 16 | fn metadata() { 17 | let metadata = MetadataCommand::new().no_deps().exec().unwrap(); 18 | 19 | let this = &metadata.packages[0]; 20 | assert_eq!(this.name.as_str(), "cargo_metadata"); 21 | assert_eq!(this.targets.len(), 3); 22 | 23 | let lib = this 24 | .targets 25 | .iter() 26 | .find(|t| t.name == "cargo_metadata") 27 | .unwrap(); 28 | assert_eq!(lib.kind[0], "lib".into()); 29 | assert_eq!(lib.crate_types[0], "lib".into()); 30 | 31 | let selftest = this.targets.iter().find(|t| t.name == "selftest").unwrap(); 32 | assert_eq!(selftest.name, "selftest"); 33 | assert_eq!(selftest.kind[0], "test".into()); 34 | assert_eq!(selftest.crate_types[0], "bin".into()); 35 | 36 | let package_metadata = &metadata.packages[0] 37 | .metadata 38 | .as_object() 39 | .expect("package.metadata must be a table."); 40 | // The second field is docs.rs metadata, ignore it 41 | assert_eq!(package_metadata.len(), 2); 42 | 43 | let value = package_metadata.get("cargo_metadata_test").unwrap(); 44 | let test_package_metadata: TestPackageMetadata = serde_json::from_value(value.clone()).unwrap(); 45 | assert_eq!( 46 | test_package_metadata, 47 | TestPackageMetadata { 48 | some_field: true, 49 | other_field: "foo".into(), 50 | } 51 | ); 52 | } 53 | 54 | #[test] 55 | fn builder_interface() { 56 | let _ = MetadataCommand::new() 57 | .manifest_path("Cargo.toml") 58 | .exec() 59 | .unwrap(); 60 | let _ = MetadataCommand::new() 61 | .manifest_path(String::from("Cargo.toml")) 62 | .exec() 63 | .unwrap(); 64 | let _ = MetadataCommand::new() 65 | .manifest_path(PathBuf::from("Cargo.toml")) 66 | .exec() 67 | .unwrap(); 68 | let _ = MetadataCommand::new() 69 | .manifest_path("Cargo.toml") 70 | .no_deps() 71 | .exec() 72 | .unwrap(); 73 | let _ = MetadataCommand::new() 74 | .manifest_path("Cargo.toml") 75 | .features(CargoOpt::AllFeatures) 76 | .exec() 77 | .unwrap(); 78 | let _ = MetadataCommand::new() 79 | .manifest_path("Cargo.toml") 80 | .current_dir(current_dir().unwrap()) 81 | .exec() 82 | .unwrap(); 83 | } 84 | 85 | #[test] 86 | fn error1() { 87 | let manifest_path = current_dir().unwrap().join("foo"); 88 | let error = "error: the manifest-path must be a path to a Cargo.toml file"; 89 | let error_with_path = format!("{error}: `{}`", manifest_path.display()); 90 | match MetadataCommand::new().manifest_path("foo").exec() { 91 | Err(Error::CargoMetadata { stderr }) => { 92 | assert!([error, &error_with_path].contains(&stderr.trim())) 93 | } 94 | _ => unreachable!(), 95 | } 96 | } 97 | 98 | #[test] 99 | fn error2() { 100 | match MetadataCommand::new() 101 | .manifest_path("foo/Cargo.toml") 102 | .exec() 103 | { 104 | Err(Error::CargoMetadata { stderr }) => assert_eq!( 105 | stderr.trim(), 106 | "error: manifest path `foo/Cargo.toml` does not exist" 107 | ), 108 | _ => unreachable!(), 109 | } 110 | } 111 | 112 | #[test] 113 | fn cargo_path() { 114 | match MetadataCommand::new() 115 | .cargo_path("this does not exist") 116 | .exec() 117 | { 118 | Err(Error::Io(e)) => assert_eq!(e.kind(), std::io::ErrorKind::NotFound), 119 | _ => unreachable!(), 120 | } 121 | } 122 | 123 | #[test] 124 | fn metadata_deps() { 125 | std::env::set_var("CARGO_PROFILE", "3"); 126 | let metadata = MetadataCommand::new() 127 | .manifest_path("Cargo.toml") 128 | .exec() 129 | .unwrap(); 130 | let this_id = metadata 131 | .workspace_members 132 | .first() 133 | .expect("Did not find ourselves"); 134 | let this = &metadata[this_id]; 135 | 136 | assert_eq!(this.name.as_str(), "cargo_metadata"); 137 | 138 | let workspace_packages = metadata.workspace_packages(); 139 | assert_eq!(workspace_packages.len(), 1); 140 | assert_eq!(&workspace_packages[0].id, this_id); 141 | 142 | let lib = this 143 | .targets 144 | .iter() 145 | .find(|t| t.name == "cargo_metadata") 146 | .unwrap(); 147 | assert_eq!(lib.kind[0], "lib".into()); 148 | assert_eq!(lib.crate_types[0], "lib".into()); 149 | 150 | let selftest = this.targets.iter().find(|t| t.name == "selftest").unwrap(); 151 | assert_eq!(selftest.name, "selftest"); 152 | assert_eq!(selftest.kind[0], "test".into()); 153 | assert_eq!(selftest.crate_types[0], "bin".into()); 154 | 155 | let dependencies = &this.dependencies; 156 | 157 | let serde = dependencies 158 | .iter() 159 | .find(|dep| dep.name == "serde") 160 | .expect("Did not find serde dependency"); 161 | 162 | assert_eq!(serde.kind, cargo_metadata::DependencyKind::Normal); 163 | assert!(!serde.req.matches(&Version::parse("1.0.0").unwrap())); 164 | assert!(serde.req.matches(&Version::parse("1.99.99").unwrap())); 165 | assert!(!serde.req.matches(&Version::parse("2.0.0").unwrap())); 166 | } 167 | 168 | #[test] 169 | fn workspace_default_packages() { 170 | let metadata = MetadataCommand::new() 171 | .manifest_path("Cargo.toml") 172 | .exec() 173 | .unwrap(); 174 | let workspace_packages = metadata.workspace_packages(); 175 | // this will only trigger on cargo versions that expose 176 | // workspace_default_members (that is, cargo >= 1.71) 177 | if metadata.workspace_default_members.is_available() { 178 | let default_packages = metadata.workspace_default_packages(); 179 | assert_eq!(default_packages, workspace_packages); 180 | } 181 | } 182 | 183 | fn cargo_version() -> semver::Version { 184 | let output = std::process::Command::new("cargo") 185 | .arg("-V") 186 | .output() 187 | .expect("Failed to exec cargo."); 188 | let out = std::str::from_utf8(&output.stdout) 189 | .expect("invalid utf8") 190 | .trim(); 191 | let split: Vec<&str> = out.split_whitespace().collect(); 192 | assert!(split.len() >= 2, "cargo -V output is unexpected: {}", out); 193 | let mut ver = semver::Version::parse(split[1]).expect("cargo -V semver could not be parsed"); 194 | // Don't care about metadata, it is awkward to compare. 195 | ver.pre = semver::Prerelease::EMPTY; 196 | ver.build = semver::BuildMetadata::EMPTY; 197 | ver 198 | } 199 | 200 | #[test] 201 | fn build_dir() { 202 | let metadata = MetadataCommand::new().no_deps().exec().unwrap(); 203 | let ver = cargo_version(); 204 | let minimum = semver::Version::parse("1.91.0").unwrap(); 205 | if ver >= minimum { 206 | assert!(&metadata.build_directory.is_some()); 207 | assert!(&metadata 208 | .build_directory 209 | .unwrap() 210 | .ends_with("cargo_metadata/target")); 211 | } else { 212 | assert!(&metadata.build_directory.is_none()); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/messages.rs: -------------------------------------------------------------------------------- 1 | use super::{Diagnostic, PackageId, Target}; 2 | use camino::Utf8PathBuf; 3 | #[cfg(feature = "builder")] 4 | use derive_builder::Builder; 5 | use serde::{de, ser, Deserialize, Serialize}; 6 | use std::fmt::{self, Write}; 7 | use std::io::{self, BufRead, Read}; 8 | 9 | /// Profile settings used to determine which compiler flags to use for a 10 | /// target. 11 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] 12 | #[cfg_attr(feature = "builder", derive(Builder))] 13 | #[non_exhaustive] 14 | #[cfg_attr(feature = "builder", builder(pattern = "owned", setter(into)))] 15 | pub struct ArtifactProfile { 16 | /// Optimization level. Possible values are 0-3, s or z. 17 | pub opt_level: String, 18 | /// The kind of debug information. 19 | #[serde(default)] 20 | pub debuginfo: ArtifactDebuginfo, 21 | /// State of the `cfg(debug_assertions)` directive, enabling macros like 22 | /// `debug_assert!` 23 | pub debug_assertions: bool, 24 | /// State of the overflow checks. 25 | pub overflow_checks: bool, 26 | /// Whether this profile is a test 27 | pub test: bool, 28 | } 29 | 30 | /// The kind of debug information included in the artifact. 31 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] 32 | #[non_exhaustive] 33 | pub enum ArtifactDebuginfo { 34 | /// No debug information. 35 | #[default] 36 | None, 37 | /// Line directives only. 38 | LineDirectivesOnly, 39 | /// Line tables only. 40 | LineTablesOnly, 41 | /// Debug information without type or variable-level information. 42 | Limited, 43 | /// Full debug information. 44 | Full, 45 | /// An unknown integer level. 46 | /// 47 | /// This may be produced by a version of rustc in the future that has 48 | /// additional levels represented by an integer that are not known by this 49 | /// version of `cargo_metadata`. 50 | UnknownInt(i64), 51 | /// An unknown string level. 52 | /// 53 | /// This may be produced by a version of rustc in the future that has 54 | /// additional levels represented by a string that are not known by this 55 | /// version of `cargo_metadata`. 56 | UnknownString(String), 57 | } 58 | 59 | impl ser::Serialize for ArtifactDebuginfo { 60 | fn serialize(&self, serializer: S) -> Result 61 | where 62 | S: ser::Serializer, 63 | { 64 | match self { 65 | Self::None => 0.serialize(serializer), 66 | Self::LineDirectivesOnly => "line-directives-only".serialize(serializer), 67 | Self::LineTablesOnly => "line-tables-only".serialize(serializer), 68 | Self::Limited => 1.serialize(serializer), 69 | Self::Full => 2.serialize(serializer), 70 | Self::UnknownInt(n) => n.serialize(serializer), 71 | Self::UnknownString(s) => s.serialize(serializer), 72 | } 73 | } 74 | } 75 | 76 | impl<'de> de::Deserialize<'de> for ArtifactDebuginfo { 77 | fn deserialize(d: D) -> Result 78 | where 79 | D: de::Deserializer<'de>, 80 | { 81 | struct Visitor; 82 | 83 | impl de::Visitor<'_> for Visitor { 84 | type Value = ArtifactDebuginfo; 85 | 86 | fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 87 | formatter.write_str("an integer or string") 88 | } 89 | 90 | fn visit_i64(self, value: i64) -> Result 91 | where 92 | E: de::Error, 93 | { 94 | let debuginfo = match value { 95 | 0 => ArtifactDebuginfo::None, 96 | 1 => ArtifactDebuginfo::Limited, 97 | 2 => ArtifactDebuginfo::Full, 98 | n => ArtifactDebuginfo::UnknownInt(n), 99 | }; 100 | Ok(debuginfo) 101 | } 102 | 103 | fn visit_u64(self, value: u64) -> Result 104 | where 105 | E: de::Error, 106 | { 107 | self.visit_i64(value as i64) 108 | } 109 | 110 | fn visit_str(self, value: &str) -> Result 111 | where 112 | E: de::Error, 113 | { 114 | let debuginfo = match value { 115 | "none" => ArtifactDebuginfo::None, 116 | "limited" => ArtifactDebuginfo::Limited, 117 | "full" => ArtifactDebuginfo::Full, 118 | "line-directives-only" => ArtifactDebuginfo::LineDirectivesOnly, 119 | "line-tables-only" => ArtifactDebuginfo::LineTablesOnly, 120 | s => ArtifactDebuginfo::UnknownString(s.to_string()), 121 | }; 122 | Ok(debuginfo) 123 | } 124 | 125 | fn visit_unit(self) -> Result 126 | where 127 | E: de::Error, 128 | { 129 | Ok(ArtifactDebuginfo::None) 130 | } 131 | } 132 | 133 | d.deserialize_any(Visitor) 134 | } 135 | } 136 | 137 | impl fmt::Display for ArtifactDebuginfo { 138 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 139 | match self { 140 | ArtifactDebuginfo::None => f.write_char('0'), 141 | ArtifactDebuginfo::Limited => f.write_char('1'), 142 | ArtifactDebuginfo::Full => f.write_char('2'), 143 | ArtifactDebuginfo::LineDirectivesOnly => f.write_str("line-directives-only"), 144 | ArtifactDebuginfo::LineTablesOnly => f.write_str("line-tables-only"), 145 | ArtifactDebuginfo::UnknownInt(n) => write!(f, "{n}"), 146 | ArtifactDebuginfo::UnknownString(s) => f.write_str(s), 147 | } 148 | } 149 | } 150 | 151 | /// A compiler-generated file. 152 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] 153 | #[cfg_attr(feature = "builder", derive(Builder))] 154 | #[non_exhaustive] 155 | #[cfg_attr(feature = "builder", builder(pattern = "owned", setter(into)))] 156 | pub struct Artifact { 157 | /// The package this artifact belongs to 158 | pub package_id: PackageId, 159 | /// Path to the `Cargo.toml` file 160 | #[serde(default)] 161 | pub manifest_path: Utf8PathBuf, 162 | /// The target this artifact was compiled for 163 | pub target: Target, 164 | /// The profile this artifact was compiled with 165 | pub profile: ArtifactProfile, 166 | /// The enabled features for this artifact 167 | pub features: Vec, 168 | /// The full paths to the generated artifacts 169 | /// (e.g. binary file and separate debug info) 170 | pub filenames: Vec, 171 | /// Path to the executable file 172 | pub executable: Option, 173 | /// If true, then the files were already generated 174 | pub fresh: bool, 175 | } 176 | 177 | /// Message left by the compiler 178 | // TODO: Better name. This one comes from machine_message.rs 179 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] 180 | #[cfg_attr(feature = "builder", derive(Builder))] 181 | #[non_exhaustive] 182 | #[cfg_attr(feature = "builder", builder(pattern = "owned", setter(into)))] 183 | pub struct CompilerMessage { 184 | /// The package this message belongs to 185 | pub package_id: PackageId, 186 | /// The target this message is aimed at 187 | pub target: Target, 188 | /// The message the compiler sent. 189 | pub message: Diagnostic, 190 | } 191 | 192 | /// Output of a build script execution. 193 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] 194 | #[cfg_attr(feature = "builder", derive(Builder))] 195 | #[non_exhaustive] 196 | #[cfg_attr(feature = "builder", builder(pattern = "owned", setter(into)))] 197 | pub struct BuildScript { 198 | /// The package this build script execution belongs to 199 | pub package_id: PackageId, 200 | /// The libs to link 201 | pub linked_libs: Vec, 202 | /// The paths to search when resolving libs 203 | pub linked_paths: Vec, 204 | /// Various `--cfg` flags to pass to the compiler 205 | pub cfgs: Vec, 206 | /// The environment variables to add to the compilation 207 | pub env: Vec<(String, String)>, 208 | /// The `OUT_DIR` environment variable where this script places its output 209 | /// 210 | /// Added in Rust 1.41. 211 | #[serde(default)] 212 | pub out_dir: Utf8PathBuf, 213 | } 214 | 215 | /// Final result of a build. 216 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] 217 | #[cfg_attr(feature = "builder", derive(Builder))] 218 | #[non_exhaustive] 219 | #[cfg_attr(feature = "builder", builder(pattern = "owned", setter(into)))] 220 | pub struct BuildFinished { 221 | /// Whether or not the build finished successfully. 222 | pub success: bool, 223 | } 224 | 225 | /// A cargo message 226 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] 227 | #[non_exhaustive] 228 | #[serde(tag = "reason", rename_all = "kebab-case")] 229 | pub enum Message { 230 | /// The compiler generated an artifact 231 | CompilerArtifact(Artifact), 232 | /// The compiler wants to display a message 233 | CompilerMessage(CompilerMessage), 234 | /// A build script successfully executed. 235 | BuildScriptExecuted(BuildScript), 236 | /// The build has finished. 237 | /// 238 | /// This is emitted at the end of the build as the last message. 239 | /// Added in Rust 1.44. 240 | BuildFinished(BuildFinished), 241 | /// A line of text which isn't a cargo or compiler message. 242 | /// Line separator is not included 243 | #[serde(skip)] 244 | TextLine(String), 245 | } 246 | 247 | impl Message { 248 | /// Creates an iterator of Message from a Read outputting a stream of JSON 249 | /// messages. For usage information, look at the top-level documentation. 250 | pub fn parse_stream(input: R) -> MessageIter { 251 | MessageIter { input } 252 | } 253 | } 254 | 255 | impl fmt::Display for CompilerMessage { 256 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 257 | write!(f, "{}", self.message) 258 | } 259 | } 260 | 261 | /// An iterator of Messages. 262 | pub struct MessageIter { 263 | input: R, 264 | } 265 | 266 | impl Iterator for MessageIter { 267 | type Item = io::Result; 268 | fn next(&mut self) -> Option { 269 | let mut line = String::new(); 270 | self.input 271 | .read_line(&mut line) 272 | .map(|n| { 273 | if n == 0 { 274 | None 275 | } else { 276 | if line.ends_with('\n') { 277 | line.truncate(line.len() - 1); 278 | } 279 | let mut deserializer = serde_json::Deserializer::from_str(&line); 280 | deserializer.disable_recursion_limit(); 281 | Some(Message::deserialize(&mut deserializer).unwrap_or(Message::TextLine(line))) 282 | } 283 | }) 284 | .transpose() 285 | } 286 | } 287 | 288 | /// An iterator of Message. 289 | type MessageIterator = 290 | serde_json::StreamDeserializer<'static, serde_json::de::IoRead, Message>; 291 | 292 | /// Creates an iterator of Message from a Read outputting a stream of JSON 293 | /// messages. For usage information, look at the top-level documentation. 294 | #[deprecated(note = "Use Message::parse_stream instead")] 295 | pub fn parse_messages(input: R) -> MessageIterator { 296 | serde_json::Deserializer::from_reader(input).into_iter::() 297 | } 298 | -------------------------------------------------------------------------------- /tests/test_samples.rs: -------------------------------------------------------------------------------- 1 | extern crate cargo_metadata; 2 | extern crate semver; 3 | #[macro_use] 4 | extern crate serde_json; 5 | 6 | use camino::Utf8PathBuf; 7 | use cargo_metadata::{ 8 | ArtifactDebuginfo, CargoOpt, DependencyKind, Edition, FeatureName, Message, Metadata, 9 | MetadataCommand, Source, 10 | }; 11 | 12 | /// Output from oldest version ever supported (1.24). 13 | /// 14 | /// This intentionally has as many null fields as possible. 15 | /// 1.8 is when metadata was introduced. 16 | /// Older versions not supported because the following are required: 17 | /// - `workspace_members` added in 1.13 18 | /// - `target_directory` added in 1.19 19 | /// - `workspace_root` added in 1.24 20 | const JSON_OLD_MINIMAL: &str = r#" 21 | { 22 | "packages": [ 23 | { 24 | "name": "foo", 25 | "version": "0.1.0", 26 | "id": "foo 0.1.0 (path+file:///foo)", 27 | "license": null, 28 | "license_file": null, 29 | "description": null, 30 | "source": null, 31 | "dependencies": [ 32 | { 33 | "name": "somedep", 34 | "source": null, 35 | "req": "^1.0", 36 | "kind": null, 37 | "optional": false, 38 | "uses_default_features": true, 39 | "features": [], 40 | "target": null 41 | } 42 | ], 43 | "targets": [ 44 | { 45 | "kind": [ 46 | "bin" 47 | ], 48 | "crate_types": [ 49 | "bin" 50 | ], 51 | "name": "foo", 52 | "src_path": "/foo/src/main.rs" 53 | } 54 | ], 55 | "features": {}, 56 | "manifest_path": "/foo/Cargo.toml" 57 | } 58 | ], 59 | "workspace_members": [ 60 | "foo 0.1.0 (path+file:///foo)" 61 | ], 62 | "resolve": null, 63 | "target_directory": "/foo/target", 64 | "version": 1, 65 | "workspace_root": "/foo" 66 | } 67 | "#; 68 | 69 | #[test] 70 | fn old_minimal() { 71 | let meta: Metadata = serde_json::from_str(JSON_OLD_MINIMAL).unwrap(); 72 | assert_eq!(meta.packages.len(), 1); 73 | let pkg = &meta.packages[0]; 74 | assert_eq!(pkg.name.as_str(), "foo"); 75 | assert_eq!(pkg.version, semver::Version::parse("0.1.0").unwrap()); 76 | assert_eq!(pkg.authors.len(), 0); 77 | assert_eq!(pkg.id.to_string(), "foo 0.1.0 (path+file:///foo)"); 78 | assert_eq!(pkg.description, None); 79 | assert_eq!(pkg.license, None); 80 | assert_eq!(pkg.license_file, None); 81 | assert_eq!(pkg.default_run, None); 82 | assert_eq!(pkg.rust_version, None); 83 | assert_eq!(pkg.dependencies.len(), 1); 84 | let dep = &pkg.dependencies[0]; 85 | assert_eq!(dep.name, "somedep"); 86 | assert_eq!(dep.source, None); 87 | assert_eq!(dep.req, semver::VersionReq::parse("^1.0").unwrap()); 88 | assert_eq!(dep.kind, DependencyKind::Normal); 89 | assert!(!dep.optional); 90 | assert!(dep.uses_default_features); 91 | assert_eq!(dep.features.len(), 0); 92 | assert!(dep.target.is_none()); 93 | assert_eq!(dep.rename, None); 94 | assert_eq!(dep.registry, None); 95 | assert_eq!(pkg.targets.len(), 1); 96 | let target = &pkg.targets[0]; 97 | assert_eq!(target.name, "foo"); 98 | assert_eq!(target.kind, vec!["bin".into()]); 99 | assert_eq!(target.crate_types, vec!["bin".into()]); 100 | assert_eq!(target.required_features.len(), 0); 101 | assert_eq!(target.src_path, "/foo/src/main.rs"); 102 | assert_eq!(target.edition, Edition::E2015); 103 | assert!(target.doctest); 104 | assert!(target.test); 105 | assert!(target.doc); 106 | assert_eq!(pkg.features.len(), 0); 107 | assert_eq!(pkg.manifest_path, "/foo/Cargo.toml"); 108 | assert_eq!(pkg.categories.len(), 0); 109 | assert_eq!(pkg.keywords.len(), 0); 110 | assert_eq!(pkg.readme, None); 111 | assert_eq!(pkg.repository, None); 112 | assert_eq!(pkg.homepage, None); 113 | assert_eq!(pkg.documentation, None); 114 | assert_eq!(pkg.edition, Edition::E2015); 115 | assert_eq!(pkg.metadata, serde_json::Value::Null); 116 | assert_eq!(pkg.links, None); 117 | assert_eq!(pkg.publish, None); 118 | assert_eq!(meta.workspace_members.len(), 1); 119 | assert_eq!( 120 | meta.workspace_members[0].to_string(), 121 | "foo 0.1.0 (path+file:///foo)" 122 | ); 123 | assert!(meta.resolve.is_none()); 124 | assert_eq!(meta.workspace_root, "/foo"); 125 | assert_eq!(meta.workspace_metadata, serde_json::Value::Null); 126 | assert_eq!(meta.target_directory, "/foo/target"); 127 | 128 | assert!(!meta.workspace_default_members.is_available()); 129 | assert!(meta.workspace_default_members.is_missing()); 130 | 131 | let serialized = serde_json::to_value(meta).unwrap(); 132 | assert!(!serialized 133 | .as_object() 134 | .unwrap() 135 | .contains_key("workspace_default_members")); 136 | } 137 | 138 | macro_rules! sorted { 139 | ($e:expr) => {{ 140 | let mut v = $e.clone(); 141 | v.sort(); 142 | v 143 | }}; 144 | } 145 | 146 | macro_rules! features { 147 | ($($feat:expr),* $(,)?) => { 148 | ::std::vec![ 149 | $(FeatureName::new(String::from($feat))),* 150 | ] 151 | }; 152 | } 153 | 154 | fn cargo_version() -> semver::Version { 155 | let output = std::process::Command::new("cargo") 156 | .arg("-V") 157 | .output() 158 | .expect("Failed to exec cargo."); 159 | let out = std::str::from_utf8(&output.stdout) 160 | .expect("invalid utf8") 161 | .trim(); 162 | let split: Vec<&str> = out.split_whitespace().collect(); 163 | assert!(split.len() >= 2, "cargo -V output is unexpected: {}", out); 164 | let mut ver = semver::Version::parse(split[1]).expect("cargo -V semver could not be parsed"); 165 | // Don't care about metadata, it is awkward to compare. 166 | ver.pre = semver::Prerelease::EMPTY; 167 | ver.build = semver::BuildMetadata::EMPTY; 168 | ver 169 | } 170 | 171 | #[derive(serde::Deserialize, PartialEq, Eq, Debug)] 172 | struct WorkspaceMetadata { 173 | testobject: TestObject, 174 | } 175 | 176 | #[derive(serde::Deserialize, PartialEq, Eq, Debug)] 177 | struct TestObject { 178 | myvalue: String, 179 | } 180 | 181 | #[test] 182 | fn all_the_fields() { 183 | // All the fields currently generated as of 1.60. This tries to exercise as 184 | // much as possible. 185 | let ver = cargo_version(); 186 | let minimum = semver::Version::parse("1.56.0").unwrap(); 187 | if ver < minimum { 188 | // edition added in 1.30 189 | // rename added in 1.31 190 | // links added in 1.33 191 | // doctest added in 1.37 192 | // publish added in 1.39 193 | // dep_kinds added in 1.41 194 | // test added in 1.47 195 | // homepage added in 1.49 196 | // documentation added in 1.49 197 | // doc added in 1.50 198 | // path added in 1.51 199 | // default_run added in 1.55 200 | // rust_version added in 1.58 201 | // workspace_default_members added in 1.71 202 | eprintln!("Skipping all_the_fields test, cargo {} is too old.", ver); 203 | return; 204 | } 205 | let meta = MetadataCommand::new() 206 | .manifest_path("tests/all/Cargo.toml") 207 | .exec() 208 | .unwrap(); 209 | assert_eq!(meta.workspace_root.file_name().unwrap(), "all"); 210 | assert_eq!( 211 | serde_json::from_value::(meta.workspace_metadata.clone()).unwrap(), 212 | WorkspaceMetadata { 213 | testobject: TestObject { 214 | myvalue: "abc".to_string() 215 | } 216 | } 217 | ); 218 | assert_eq!(meta.workspace_members.len(), 1); 219 | assert!(meta.workspace_members[0].to_string().contains("all")); 220 | if ver >= semver::Version::parse("1.71.0").unwrap() { 221 | assert_eq!(&*meta.workspace_default_members, &meta.workspace_members); 222 | } 223 | 224 | assert_eq!(meta.packages.len(), 9); 225 | let all = meta 226 | .packages 227 | .iter() 228 | .find(|p| p.name.as_str() == "all") 229 | .unwrap(); 230 | assert_eq!(all.version, semver::Version::parse("0.1.0").unwrap()); 231 | assert_eq!(all.authors, vec!["Jane Doe "]); 232 | assert!(all.id.to_string().contains("all")); 233 | assert_eq!(all.description, Some("Package description.".to_string())); 234 | assert_eq!(all.license, Some("MIT/Apache-2.0".to_string())); 235 | assert_eq!(all.license_file, Some(Utf8PathBuf::from("LICENSE"))); 236 | assert!(all.license_file().unwrap().ends_with("tests/all/LICENSE")); 237 | assert_eq!(all.publish, Some(vec![])); 238 | assert_eq!(all.links, Some("foo".to_string())); 239 | assert_eq!(all.default_run, Some("otherbin".to_string())); 240 | if ver >= semver::Version::parse("1.58.0").unwrap() { 241 | assert_eq!( 242 | all.rust_version, 243 | Some(semver::Version::parse("1.56.0").unwrap()) 244 | ); 245 | } 246 | 247 | assert_eq!(all.dependencies.len(), 8); 248 | let bitflags = all 249 | .dependencies 250 | .iter() 251 | .find(|d| d.name == "bitflags") 252 | .unwrap(); 253 | assert_eq!( 254 | bitflags.source, 255 | Some(Source { 256 | repr: "registry+https://github.com/rust-lang/crates.io-index".to_string() 257 | }) 258 | ); 259 | assert!(bitflags.optional); 260 | assert_eq!(bitflags.req, semver::VersionReq::parse("^1.0").unwrap()); 261 | 262 | let path_dep = all 263 | .dependencies 264 | .iter() 265 | .find(|d| d.name == "path-dep") 266 | .unwrap(); 267 | assert_eq!(path_dep.source, None); 268 | assert_eq!(path_dep.kind, DependencyKind::Normal); 269 | assert_eq!(path_dep.req, semver::VersionReq::parse("*").unwrap()); 270 | assert_eq!( 271 | path_dep.path.as_ref().map(|p| p.ends_with("path-dep")), 272 | Some(true), 273 | ); 274 | 275 | all.dependencies 276 | .iter() 277 | .find(|d| d.name == "namedep") 278 | .unwrap(); 279 | 280 | let featdep = all 281 | .dependencies 282 | .iter() 283 | .find(|d| d.name == "featdep") 284 | .unwrap(); 285 | assert_eq!(featdep.features, vec!["i128"]); 286 | assert!(!featdep.uses_default_features); 287 | 288 | let renamed = all 289 | .dependencies 290 | .iter() 291 | .find(|d| d.name == "oldname") 292 | .unwrap(); 293 | assert_eq!(renamed.rename, Some("newname".to_string())); 294 | 295 | let devdep = all 296 | .dependencies 297 | .iter() 298 | .find(|d| d.name == "devdep") 299 | .unwrap(); 300 | assert_eq!(devdep.kind, DependencyKind::Development); 301 | 302 | let bdep = all.dependencies.iter().find(|d| d.name == "bdep").unwrap(); 303 | assert_eq!(bdep.kind, DependencyKind::Build); 304 | 305 | let windep = all 306 | .dependencies 307 | .iter() 308 | .find(|d| d.name == "windep") 309 | .unwrap(); 310 | assert_eq!( 311 | windep.target.as_ref().map(|x| x.to_string()), 312 | Some("cfg(windows)".to_string()) 313 | ); 314 | 315 | macro_rules! get_file_name { 316 | ($v:expr) => { 317 | all.targets 318 | .iter() 319 | .find(|t| t.src_path.file_name().unwrap() == $v) 320 | .unwrap() 321 | }; 322 | } 323 | assert_eq!(all.targets.len(), 9); 324 | let lib = get_file_name!("lib.rs"); 325 | assert_eq!(lib.name, "all"); 326 | assert_eq!( 327 | sorted!(lib.kind), 328 | vec!["cdylib".into(), "rlib".into(), "staticlib".into()] 329 | ); 330 | assert_eq!( 331 | sorted!(lib.crate_types), 332 | vec!["cdylib".into(), "rlib".into(), "staticlib".into()] 333 | ); 334 | assert_eq!(lib.required_features.len(), 0); 335 | assert_eq!(lib.edition, Edition::E2018); 336 | assert!(lib.doctest); 337 | assert!(lib.test); 338 | assert!(lib.doc); 339 | 340 | let main = get_file_name!("main.rs"); 341 | assert_eq!(main.crate_types, vec!["bin".into()]); 342 | assert_eq!(main.kind, vec!["bin".into()]); 343 | assert!(!main.doctest); 344 | assert!(main.test); 345 | assert!(main.doc); 346 | 347 | let otherbin = get_file_name!("otherbin.rs"); 348 | assert_eq!(otherbin.edition, Edition::E2015); 349 | assert!(!otherbin.doc); 350 | 351 | let reqfeat = get_file_name!("reqfeat.rs"); 352 | assert_eq!(reqfeat.required_features, vec!["feat2"]); 353 | 354 | let reqfeat_slash = get_file_name!("reqfeat_slash.rs"); 355 | assert_eq!(reqfeat_slash.required_features, vec!["featdep/i128"]); 356 | 357 | let ex1 = get_file_name!("ex1.rs"); 358 | assert_eq!(ex1.kind, vec!["example".into()]); 359 | assert!(!ex1.test); 360 | 361 | let t1 = get_file_name!("t1.rs"); 362 | assert_eq!(t1.kind, vec!["test".into()]); 363 | 364 | let b1 = get_file_name!("b1.rs"); 365 | assert_eq!(b1.kind, vec!["bench".into()]); 366 | 367 | let build = get_file_name!("build.rs"); 368 | assert_eq!(build.kind, vec!["custom-build".into()]); 369 | 370 | if ver >= semver::Version::parse("1.60.0").unwrap() { 371 | // 1.60 now reports optional dependencies within the features table 372 | assert_eq!(all.features.len(), 4); 373 | assert_eq!(all.features["bitflags"], vec!["dep:bitflags"]); 374 | } else { 375 | assert_eq!(all.features.len(), 3); 376 | } 377 | assert_eq!(all.features["feat1"].len(), 0); 378 | assert_eq!(all.features["feat2"].len(), 0); 379 | assert_eq!(sorted!(all.features["default"]), vec!["bitflags", "feat1"]); 380 | 381 | assert!(all.manifest_path.ends_with("all/Cargo.toml")); 382 | assert_eq!(all.categories, vec!["command-line-utilities"]); 383 | assert_eq!(all.keywords, vec!["cli"]); 384 | assert_eq!(all.readme, Some(Utf8PathBuf::from("README.md"))); 385 | assert!(all.readme().unwrap().ends_with("tests/all/README.md")); 386 | assert_eq!( 387 | all.repository, 388 | Some("https://github.com/oli-obk/cargo_metadata/".to_string()) 389 | ); 390 | assert_eq!( 391 | all.homepage, 392 | Some("https://github.com/oli-obk/cargo_metadata/".to_string()) 393 | ); 394 | assert_eq!( 395 | all.documentation, 396 | Some("https://docs.rs/cargo_metadata/".to_string()) 397 | ); 398 | assert_eq!(all.edition, Edition::E2018); 399 | assert_eq!( 400 | all.metadata, 401 | json!({ 402 | "docs": { 403 | "rs": { 404 | "all-features": true, 405 | "default-target": "x86_64-unknown-linux-gnu", 406 | "rustc-args": ["--example-rustc-arg"] 407 | } 408 | } 409 | }) 410 | ); 411 | 412 | let resolve = meta.resolve.as_ref().unwrap(); 413 | assert!(resolve.root.as_ref().unwrap().to_string().contains("all")); 414 | 415 | assert_eq!(resolve.nodes.len(), 9); 416 | let path_dep = resolve 417 | .nodes 418 | .iter() 419 | .find(|n| n.id.to_string().contains("path-dep")) 420 | .unwrap(); 421 | assert_eq!(path_dep.deps.len(), 0); 422 | assert_eq!(path_dep.dependencies.len(), 0); 423 | assert_eq!(path_dep.features.len(), 0); 424 | 425 | let bitflags = resolve 426 | .nodes 427 | .iter() 428 | .find(|n| n.id.to_string().contains("bitflags")) 429 | .unwrap(); 430 | assert_eq!(bitflags.features, features!["default"]); 431 | 432 | let featdep = resolve 433 | .nodes 434 | .iter() 435 | .find(|n| n.id.to_string().contains("featdep")) 436 | .unwrap(); 437 | assert_eq!(featdep.features, features!["i128"]); 438 | 439 | let all = resolve 440 | .nodes 441 | .iter() 442 | .find(|n| n.id.to_string().contains("all")) 443 | .unwrap(); 444 | assert_eq!(all.dependencies.len(), 8); 445 | assert_eq!(all.deps.len(), 8); 446 | let newname = all.deps.iter().find(|d| &*d.name == "newname").unwrap(); 447 | assert!(newname.pkg.to_string().contains("oldname")); 448 | // Note the underscore here. 449 | let path_dep = all.deps.iter().find(|d| &*d.name == "path_dep").unwrap(); 450 | assert!(path_dep.pkg.to_string().contains("path-dep")); 451 | assert_eq!(path_dep.dep_kinds.len(), 1); 452 | let kind = &path_dep.dep_kinds[0]; 453 | assert_eq!(kind.kind, DependencyKind::Normal); 454 | assert!(kind.target.is_none()); 455 | 456 | let namedep = all 457 | .deps 458 | .iter() 459 | .find(|d| &*d.name == "different_name") 460 | .unwrap(); 461 | assert!(namedep.pkg.to_string().contains("namedep")); 462 | assert_eq!( 463 | sorted!(all.features), 464 | features!["bitflags", "default", "feat1"] 465 | ); 466 | 467 | let bdep = all.deps.iter().find(|d| &*d.name == "bdep").unwrap(); 468 | assert_eq!(bdep.dep_kinds.len(), 1); 469 | let kind = &bdep.dep_kinds[0]; 470 | assert_eq!(kind.kind, DependencyKind::Build); 471 | assert!(kind.target.is_none()); 472 | 473 | let devdep = all.deps.iter().find(|d| &*d.name == "devdep").unwrap(); 474 | assert_eq!(devdep.dep_kinds.len(), 1); 475 | let kind = &devdep.dep_kinds[0]; 476 | assert_eq!(kind.kind, DependencyKind::Development); 477 | assert!(kind.target.is_none()); 478 | 479 | let windep = all.deps.iter().find(|d| &*d.name == "windep").unwrap(); 480 | assert_eq!(windep.dep_kinds.len(), 1); 481 | let kind = &windep.dep_kinds[0]; 482 | assert_eq!(kind.kind, DependencyKind::Normal); 483 | assert_eq!( 484 | kind.target.as_ref().map(|x| x.to_string()), 485 | Some("cfg(windows)".to_string()) 486 | ); 487 | 488 | let serialized = serde_json::to_value(meta).unwrap(); 489 | if ver >= semver::Version::parse("1.71.0").unwrap() { 490 | assert!(serialized.as_object().unwrap()["workspace_default_members"] 491 | .as_array() 492 | .is_some()); 493 | } else { 494 | assert!(!serialized 495 | .as_object() 496 | .unwrap() 497 | .contains_key("workspace_default_members")); 498 | } 499 | } 500 | 501 | #[test] 502 | fn alt_registry() { 503 | // This is difficult to test (would need to set up a custom index). 504 | // Just manually check the JSON is handled. 505 | let json = r#" 506 | { 507 | "packages": [ 508 | { 509 | "name": "alt", 510 | "version": "0.1.0", 511 | "id": "alt 0.1.0 (path+file:///alt)", 512 | "source": null, 513 | "dependencies": [ 514 | { 515 | "name": "alt2", 516 | "source": "registry+https://example.com", 517 | "req": "^0.1", 518 | "kind": null, 519 | "rename": null, 520 | "optional": false, 521 | "uses_default_features": true, 522 | "features": [], 523 | "target": null, 524 | "registry": "https://example.com" 525 | } 526 | ], 527 | "targets": [ 528 | { 529 | "kind": [ 530 | "lib" 531 | ], 532 | "crate_types": [ 533 | "lib" 534 | ], 535 | "name": "alt", 536 | "src_path": "/alt/src/lib.rs", 537 | "edition": "2018" 538 | } 539 | ], 540 | "features": {}, 541 | "manifest_path": "/alt/Cargo.toml", 542 | "metadata": null, 543 | "authors": [], 544 | "categories": [], 545 | "keywords": [], 546 | "readme": null, 547 | "repository": null, 548 | "edition": "2018", 549 | "links": null 550 | } 551 | ], 552 | "workspace_members": [ 553 | "alt 0.1.0 (path+file:///alt)" 554 | ], 555 | "resolve": null, 556 | "target_directory": "/alt/target", 557 | "version": 1, 558 | "workspace_root": "/alt" 559 | } 560 | "#; 561 | let meta: Metadata = serde_json::from_str(json).unwrap(); 562 | assert_eq!(meta.packages.len(), 1); 563 | let alt = &meta.packages[0]; 564 | let deps = &alt.dependencies; 565 | assert_eq!(deps.len(), 1); 566 | let dep = &deps[0]; 567 | assert_eq!(dep.registry, Some("https://example.com".to_string())); 568 | } 569 | 570 | #[test] 571 | fn current_dir() { 572 | let meta = MetadataCommand::new() 573 | .current_dir("tests/all/namedep") 574 | .exec() 575 | .unwrap(); 576 | let namedep = meta 577 | .packages 578 | .iter() 579 | .find(|p| p.name.as_str() == "namedep") 580 | .unwrap(); 581 | assert!(namedep.name.starts_with("namedep")); 582 | } 583 | 584 | #[test] 585 | fn parse_stream_is_robust() { 586 | // Proc macros can print stuff to stdout, which naturally breaks JSON messages. 587 | // Let's check that we don't die horribly in this case, and report an error. 588 | let json_output = r##"{"reason":"compiler-artifact","package_id":"chatty 0.1.0 (path+file:///chatty-macro/chatty)","manifest_path":"chatty-macro/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"chatty","src_path":"/chatty-macro/chatty/src/lib.rs","edition":"2018","doctest":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/chatty-macro/target/debug/deps/libchatty-f2adcff24cdf3bb2.so"],"executable":null,"fresh":false} 589 | Evil proc macro was here! 590 | {"reason":"compiler-artifact","package_id":"chatty-macro 0.1.0 (path+file:///chatty-macro)","manifest_path":"chatty-macro/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"chatty-macro","src_path":"/chatty-macro/src/lib.rs","edition":"2018","doctest":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/chatty-macro/target/debug/libchatty_macro.rlib","/chatty-macro/target/debug/deps/libchatty_macro-cb5956ed52a11fb6.rmeta"],"executable":null,"fresh":false} 591 | "##; 592 | let mut n_messages = 0; 593 | let mut text = String::new(); 594 | for message in cargo_metadata::Message::parse_stream(json_output.as_bytes()) { 595 | let message = message.unwrap(); 596 | match message { 597 | cargo_metadata::Message::TextLine(line) => text = line, 598 | _ => n_messages += 1, 599 | } 600 | } 601 | assert_eq!(n_messages, 2); 602 | assert_eq!(text, "Evil proc macro was here!"); 603 | } 604 | 605 | #[test] 606 | fn advanced_feature_configuration() { 607 | fn build_features &mut MetadataCommand>( 608 | func: F, 609 | ) -> Vec { 610 | let mut meta = MetadataCommand::new(); 611 | let meta = meta.manifest_path("tests/all/Cargo.toml"); 612 | 613 | let meta = func(meta); 614 | let meta = meta.exec().unwrap(); 615 | 616 | let resolve = meta.resolve.as_ref().unwrap(); 617 | 618 | let all = resolve 619 | .nodes 620 | .iter() 621 | .find(|n| !n.features.is_empty()) 622 | .unwrap(); 623 | 624 | all.features 625 | .clone() 626 | .into_iter() 627 | .map(FeatureName::into_inner) 628 | .collect() 629 | } 630 | 631 | // Default behavior; tested above 632 | let default_features = build_features(|meta| meta); 633 | assert_eq!( 634 | sorted!(default_features), 635 | vec!["bitflags", "default", "feat1"] 636 | ); 637 | 638 | // Manually specify the same default features 639 | let manual_features = build_features(|meta| { 640 | meta.features(CargoOpt::NoDefaultFeatures) 641 | .features(CargoOpt::SomeFeatures(vec![ 642 | "feat1".into(), 643 | "bitflags".into(), 644 | ])) 645 | }); 646 | assert_eq!(sorted!(manual_features), vec!["bitflags", "feat1"]); 647 | 648 | // Multiple SomeFeatures is same as one longer SomeFeatures 649 | let manual_features = build_features(|meta| { 650 | meta.features(CargoOpt::NoDefaultFeatures) 651 | .features(CargoOpt::SomeFeatures(vec!["feat1".into()])) 652 | .features(CargoOpt::SomeFeatures(vec!["feat2".into()])) 653 | }); 654 | assert_eq!(sorted!(manual_features), vec!["feat1", "feat2"]); 655 | 656 | // No features + All features == All features 657 | let all_features = build_features(|meta| { 658 | meta.features(CargoOpt::AllFeatures) 659 | .features(CargoOpt::NoDefaultFeatures) 660 | }); 661 | assert_eq!( 662 | sorted!(all_features), 663 | vec!["bitflags", "default", "feat1", "feat2"] 664 | ); 665 | 666 | // The '--all-features' flag supersedes other feature flags 667 | let all_flag_variants = build_features(|meta| { 668 | meta.features(CargoOpt::SomeFeatures(vec!["feat2".into()])) 669 | .features(CargoOpt::NoDefaultFeatures) 670 | .features(CargoOpt::AllFeatures) 671 | }); 672 | assert_eq!(sorted!(all_flag_variants), sorted!(all_features)); 673 | } 674 | 675 | #[test] 676 | fn depkind_to_string() { 677 | assert_eq!(DependencyKind::Normal.to_string(), "normal"); 678 | assert_eq!(DependencyKind::Development.to_string(), "dev"); 679 | assert_eq!(DependencyKind::Build.to_string(), "build"); 680 | assert_eq!(DependencyKind::Unknown.to_string(), "Unknown"); 681 | } 682 | 683 | #[test] 684 | fn basic_workspace_root_package_exists() { 685 | // First try with dependencies 686 | let meta = MetadataCommand::new() 687 | .manifest_path("tests/basic_workspace/Cargo.toml") 688 | .exec() 689 | .unwrap(); 690 | assert_eq!(meta.root_package().unwrap().name.as_str(), "ex_bin"); 691 | // Now with no_deps, it should still work exactly the same 692 | let meta = MetadataCommand::new() 693 | .manifest_path("tests/basic_workspace/Cargo.toml") 694 | .no_deps() 695 | .exec() 696 | .unwrap(); 697 | assert_eq!( 698 | meta.root_package() 699 | .expect("workspace root still exists when no_deps used") 700 | .name 701 | .as_str(), 702 | "ex_bin" 703 | ); 704 | } 705 | 706 | #[test] 707 | fn debuginfo_variants() { 708 | // Checks behavior for the different debuginfo variants. 709 | let variants = [ 710 | ("0", ArtifactDebuginfo::None), 711 | ("1", ArtifactDebuginfo::Limited), 712 | ("2", ArtifactDebuginfo::Full), 713 | ( 714 | "\"line-directives-only\"", 715 | ArtifactDebuginfo::LineDirectivesOnly, 716 | ), 717 | ("\"line-tables-only\"", ArtifactDebuginfo::LineTablesOnly), 718 | ("3", ArtifactDebuginfo::UnknownInt(3)), 719 | ( 720 | "\"abc\"", 721 | ArtifactDebuginfo::UnknownString("abc".to_string()), 722 | ), 723 | ("null", ArtifactDebuginfo::None), 724 | ]; 725 | for (value, expected) in variants { 726 | let s = r#"{"reason":"compiler-artifact","package_id":"cargo_metadata 0.16.0 (path+file:////cargo_metadata)","manifest_path":"/cargo_metadata/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cargo_metadata","src_path":"/cargo_metadata/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":DEBUGINFO,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/cargo_metadata/target/debug/deps/libcargo_metadata-27f582f7187b9a2c.rmeta"],"executable":null,"fresh":false}"#; 727 | let message: Message = serde_json::from_str(&s.replace("DEBUGINFO", value)).unwrap(); 728 | match message { 729 | Message::CompilerArtifact(artifact) => { 730 | assert_eq!(artifact.profile.debuginfo, expected); 731 | let de_s = serde_json::to_string(&artifact.profile.debuginfo).unwrap(); 732 | // Note: Roundtrip does not retain null value. 733 | if value == "null" { 734 | assert_eq!(artifact.profile.debuginfo.to_string(), "0"); 735 | assert_eq!(de_s, "0"); 736 | } else { 737 | assert_eq!( 738 | artifact.profile.debuginfo.to_string(), 739 | value.trim_matches('"') 740 | ); 741 | assert_eq!(de_s, value); 742 | } 743 | } 744 | _ => panic!("unexpected {:?}", message), 745 | } 746 | } 747 | } 748 | 749 | #[test] 750 | #[should_panic = "WorkspaceDefaultMembers should only be dereferenced on Cargo versions >= 1.71"] 751 | fn missing_workspace_default_members() { 752 | let meta: Metadata = serde_json::from_str(JSON_OLD_MINIMAL).unwrap(); 753 | let _ = &*meta.workspace_default_members; 754 | } 755 | 756 | #[test] 757 | fn workspace_default_members_is_available() { 758 | // generated with cargo +1.71.0 metadata --format-version 1 759 | let json = r#" 760 | { 761 | "packages": [ 762 | { 763 | "name": "basic", 764 | "version": "0.1.0", 765 | "id": "basic 0.1.0 (path+file:///example)", 766 | "license": null, 767 | "license_file": null, 768 | "description": null, 769 | "source": null, 770 | "dependencies": [], 771 | "targets": [ 772 | { 773 | "kind": [ 774 | "lib" 775 | ], 776 | "crate_types": [ 777 | "lib" 778 | ], 779 | "name": "basic", 780 | "src_path": "/example/src/lib.rs", 781 | "edition": "2021", 782 | "doc": true, 783 | "doctest": true, 784 | "test": true 785 | } 786 | ], 787 | "features": {}, 788 | "manifest_path": "/example/Cargo.toml", 789 | "metadata": null, 790 | "publish": null, 791 | "authors": [], 792 | "categories": [], 793 | "keywords": [], 794 | "readme": null, 795 | "repository": null, 796 | "homepage": null, 797 | "documentation": null, 798 | "edition": "2021", 799 | "links": null, 800 | "default_run": null, 801 | "rust_version": null 802 | } 803 | ], 804 | "workspace_members": [ 805 | "basic 0.1.0 (path+file:///example)" 806 | ], 807 | "workspace_default_members": [ 808 | "basic 0.1.0 (path+file:///example)" 809 | ], 810 | "resolve": { 811 | "nodes": [ 812 | { 813 | "id": "basic 0.1.0 (path+file:///example)", 814 | "dependencies": [], 815 | "deps": [], 816 | "features": [] 817 | } 818 | ], 819 | "root": "basic 0.1.0 (path+file:///example)" 820 | }, 821 | "target_directory": "/example/target", 822 | "version": 1, 823 | "workspace_root": "/example", 824 | "metadata": null 825 | } 826 | "#; 827 | 828 | let meta: Metadata = serde_json::from_str(json).unwrap(); 829 | 830 | assert!(meta.workspace_default_members.is_available()); 831 | assert!(!meta.workspace_default_members.is_missing()); 832 | } 833 | 834 | #[test] 835 | fn workspace_default_members_is_missing() { 836 | // generated with cargo +1.70.0 metadata --format-version 1 837 | let json = r#" 838 | { 839 | "packages": [ 840 | { 841 | "name": "basic", 842 | "version": "0.1.0", 843 | "id": "basic 0.1.0 (path+file:///example)", 844 | "license": null, 845 | "license_file": null, 846 | "description": null, 847 | "source": null, 848 | "dependencies": [], 849 | "targets": [ 850 | { 851 | "kind": [ 852 | "lib" 853 | ], 854 | "crate_types": [ 855 | "lib" 856 | ], 857 | "name": "basic", 858 | "src_path": "/example/src/lib.rs", 859 | "edition": "2021", 860 | "doc": true, 861 | "doctest": true, 862 | "test": true 863 | } 864 | ], 865 | "features": {}, 866 | "manifest_path": "/example/Cargo.toml", 867 | "metadata": null, 868 | "publish": null, 869 | "authors": [], 870 | "categories": [], 871 | "keywords": [], 872 | "readme": null, 873 | "repository": null, 874 | "homepage": null, 875 | "documentation": null, 876 | "edition": "2021", 877 | "links": null, 878 | "default_run": null, 879 | "rust_version": null 880 | } 881 | ], 882 | "workspace_members": [ 883 | "basic 0.1.0 (path+file:///example)" 884 | ], 885 | "resolve": { 886 | "nodes": [ 887 | { 888 | "id": "basic 0.1.0 (path+file:///example)", 889 | "dependencies": [], 890 | "deps": [], 891 | "features": [] 892 | } 893 | ], 894 | "root": "basic 0.1.0 (path+file:///example)" 895 | }, 896 | "target_directory": "/example/target", 897 | "version": 1, 898 | "workspace_root": "/example", 899 | "metadata": null 900 | } 901 | "#; 902 | 903 | let meta: Metadata = serde_json::from_str(json).unwrap(); 904 | 905 | assert!(!meta.workspace_default_members.is_available()); 906 | assert!(meta.workspace_default_members.is_missing()); 907 | } 908 | 909 | #[test] 910 | fn test_unknown_target_kind_and_crate_type() { 911 | // Both kind and crate_type set to a type not yet known 912 | let json = r#" 913 | { 914 | "packages": [ 915 | { 916 | "name": "alt", 917 | "version": "0.1.0", 918 | "id": "alt 0.1.0 (path+file:///alt)", 919 | "source": null, 920 | "dependencies": [], 921 | "targets": [ 922 | { 923 | "kind": [ 924 | "future-kind" 925 | ], 926 | "crate_types": [ 927 | "future-type" 928 | ], 929 | "name": "alt", 930 | "src_path": "/alt/src/lib.rs", 931 | "edition": "2018" 932 | } 933 | ], 934 | "features": {}, 935 | "manifest_path": "/alt/Cargo.toml", 936 | "metadata": null, 937 | "authors": [], 938 | "categories": [], 939 | "keywords": [], 940 | "readme": null, 941 | "repository": null, 942 | "edition": "2018", 943 | "links": null 944 | } 945 | ], 946 | "workspace_members": [ 947 | "alt 0.1.0 (path+file:///alt)" 948 | ], 949 | "resolve": null, 950 | "target_directory": "/alt/target", 951 | "version": 1, 952 | "workspace_root": "/alt" 953 | } 954 | "#; 955 | let meta: Metadata = serde_json::from_str(json).unwrap(); 956 | assert_eq!(meta.packages.len(), 1); 957 | assert_eq!(meta.packages[0].targets.len(), 1); 958 | let target = &meta.packages[0].targets[0]; 959 | assert_eq!(target.kind[0], "future-kind".into()); 960 | assert_eq!(target.crate_types[0], "future-type".into()); 961 | } 962 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(docsrs, feature(doc_cfg))] 2 | #![deny(missing_docs)] 3 | //! Structured access to the output of `cargo metadata` and `cargo --message-format=json`. 4 | //! Usually used from within a `cargo-*` executable 5 | //! 6 | //! See the [cargo book](https://doc.rust-lang.org/cargo/index.html) for 7 | //! details on cargo itself. 8 | //! 9 | //! ## Examples 10 | //! 11 | //! Get the current crate's metadata without default features but with all dependency information. 12 | //! 13 | //! ```rust 14 | //! # use std::path::Path; 15 | //! # use cargo_metadata::{MetadataCommand, CargoOpt}; 16 | //! let _metadata = MetadataCommand::new().exec().unwrap(); 17 | //! ``` 18 | //! 19 | //! 20 | //! If you have a program that takes `--manifest-path` as an argument, you can forward that 21 | //! to [MetadataCommand]: 22 | //! 23 | //! ```rust 24 | //! # use cargo_metadata::MetadataCommand; 25 | //! # use std::path::Path; 26 | //! let mut args = std::env::args().skip_while(|val| !val.starts_with("--manifest-path")); 27 | //! let mut cmd = MetadataCommand::new(); 28 | //! let manifest_path = match args.next() { 29 | //! Some(ref p) if p == "--manifest-path" => { 30 | //! cmd.manifest_path(args.next().unwrap()); 31 | //! } 32 | //! Some(p) => { 33 | //! cmd.manifest_path(p.trim_start_matches("--manifest-path=")); 34 | //! } 35 | //! None => {} 36 | //! }; 37 | //! 38 | //! let _metadata = cmd.exec().unwrap(); 39 | //! ``` 40 | //! 41 | //! Pass features flags, e.g. `--all-features`. 42 | //! 43 | //! ```rust 44 | //! # use std::path::Path; 45 | //! # use cargo_metadata::{MetadataCommand, CargoOpt}; 46 | //! let _metadata = MetadataCommand::new() 47 | //! .manifest_path("./Cargo.toml") 48 | //! .features(CargoOpt::AllFeatures) 49 | //! .exec() 50 | //! .unwrap(); 51 | //! ``` 52 | //! 53 | //! Parse message-format output produced by other cargo commands. 54 | //! It is recommended to use crates like `escargot` to produce the [Command]. 55 | //! 56 | //! ``` 57 | //! # use std::process::{Stdio, Command}; 58 | //! # use cargo_metadata::Message; 59 | //! let mut command = Command::new("cargo") 60 | //! .args(&["build", "--message-format=json-render-diagnostics"]) 61 | //! .stdout(Stdio::piped()) 62 | //! .spawn() 63 | //! .unwrap(); 64 | //! 65 | //! let reader = std::io::BufReader::new(command.stdout.take().unwrap()); 66 | //! for message in cargo_metadata::Message::parse_stream(reader) { 67 | //! match message.unwrap() { 68 | //! Message::CompilerMessage(msg) => { 69 | //! println!("{:?}", msg); 70 | //! }, 71 | //! Message::CompilerArtifact(artifact) => { 72 | //! println!("{:?}", artifact); 73 | //! }, 74 | //! Message::BuildScriptExecuted(script) => { 75 | //! println!("{:?}", script); 76 | //! }, 77 | //! Message::BuildFinished(finished) => { 78 | //! println!("{:?}", finished); 79 | //! }, 80 | //! _ => () // Unknown message 81 | //! } 82 | //! } 83 | //! 84 | //! let output = command.wait().expect("Couldn't get cargo's exit status"); 85 | //! ``` 86 | 87 | use camino::Utf8PathBuf; 88 | #[cfg(feature = "builder")] 89 | use derive_builder::Builder; 90 | use std::collections::BTreeMap; 91 | use std::env; 92 | use std::ffi::OsString; 93 | use std::fmt; 94 | use std::hash::Hash; 95 | use std::path::PathBuf; 96 | use std::process::{Command, Stdio}; 97 | use std::str::{from_utf8, FromStr}; 98 | 99 | pub use camino; 100 | pub use cargo_platform; 101 | pub use semver; 102 | use semver::Version; 103 | 104 | #[cfg(feature = "builder")] 105 | pub use dependency::DependencyBuilder; 106 | pub use dependency::{Dependency, DependencyKind}; 107 | use diagnostic::Diagnostic; 108 | pub use errors::{Error, Result}; 109 | #[cfg(feature = "unstable")] 110 | pub use libtest::TestMessage; 111 | #[allow(deprecated)] 112 | pub use messages::parse_messages; 113 | pub use messages::{ 114 | Artifact, ArtifactDebuginfo, ArtifactProfile, BuildFinished, BuildScript, CompilerMessage, 115 | Message, MessageIter, 116 | }; 117 | #[cfg(feature = "builder")] 118 | pub use messages::{ 119 | ArtifactBuilder, ArtifactProfileBuilder, BuildFinishedBuilder, BuildScriptBuilder, 120 | CompilerMessageBuilder, 121 | }; 122 | use serde::{Deserialize, Deserializer, Serialize}; 123 | 124 | mod dependency; 125 | pub mod diagnostic; 126 | mod errors; 127 | #[cfg(feature = "unstable")] 128 | pub mod libtest; 129 | mod messages; 130 | 131 | macro_rules! str_newtype { 132 | ( 133 | $(#[doc = $docs:literal])* 134 | $name:ident 135 | ) => { 136 | $(#[doc = $docs])* 137 | #[derive(Serialize, Debug, Clone, Eq, PartialOrd, Ord, Hash)] 138 | #[serde(transparent)] 139 | pub struct $name = String>(T); 140 | 141 | impl> $name { 142 | /// Convert the wrapped string into its inner type `T` 143 | pub fn into_inner(self) -> T { 144 | self.0 145 | } 146 | } 147 | 148 | impl> AsRef for $name { 149 | fn as_ref(&self) -> &str { 150 | self.0.as_ref() 151 | } 152 | } 153 | 154 | impl> std::ops::Deref for $name { 155 | type Target = T; 156 | 157 | fn deref(&self) -> &Self::Target { 158 | &self.0 159 | } 160 | } 161 | 162 | impl> std::borrow::Borrow for $name { 163 | fn borrow(&self) -> &str { 164 | self.0.as_ref() 165 | } 166 | } 167 | 168 | impl<'a> std::str::FromStr for $name { 169 | type Err = std::convert::Infallible; 170 | 171 | fn from_str(value: &str) -> Result { 172 | Ok(Self::new(value.to_owned())) 173 | } 174 | } 175 | 176 | impl<'de, T: AsRef + serde::Deserialize<'de>> serde::Deserialize<'de> for $name { 177 | fn deserialize(deserializer: D) -> Result 178 | where 179 | D: serde::Deserializer<'de>, 180 | { 181 | let inner = T::deserialize(deserializer)?; 182 | Ok(Self::new(inner)) 183 | } 184 | } 185 | 186 | impl> fmt::Display for $name { 187 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 188 | self.0.as_ref().fmt(f) 189 | } 190 | } 191 | 192 | // Note: The next two implementations are not based on Cargo string newtype implementations. 193 | 194 | impl> $name { 195 | /// Create a new wrapped string 196 | pub fn new(name: T) -> Self { 197 | Self(name) 198 | } 199 | } 200 | 201 | impl, Rhs: AsRef> PartialEq for $name { 202 | fn eq(&self, other: &Rhs) -> bool { 203 | self.as_ref() == other.as_ref() 204 | } 205 | } 206 | }; 207 | } 208 | 209 | str_newtype!( 210 | /// Feature name newtype 211 | /// 212 | /// Based on [cargo-util-schema's string newtype] but with two crucial differences: 213 | /// 214 | /// - This newtype does not verify the wrapped string. 215 | /// - This newtype allows comparison with arbitrary types that implement `AsRef`. 216 | /// 217 | /// [cargo-util-schema's string newtype]: https://github.com/epage/cargo/blob/d8975d2901e132c02b3f6b1d107f2f50b275a058/crates/cargo-util-schemas/src/manifest/mod.rs#L1355-L1413 218 | FeatureName 219 | ); 220 | 221 | str_newtype!( 222 | /// Package name newtype 223 | /// 224 | /// Based on [cargo-util-schema's string newtype] but with two crucial differences: 225 | /// 226 | /// - This newtype does not verify the wrapped string. 227 | /// - This newtype allows comparison with arbitrary types that implement `AsRef`. 228 | /// 229 | /// [cargo-util-schema's string newtype]: https://github.com/epage/cargo/blob/d8975d2901e132c02b3f6b1d107f2f50b275a058/crates/cargo-util-schemas/src/manifest/mod.rs#L1355-L1413 230 | PackageName 231 | ); 232 | 233 | /// An "opaque" identifier for a package. 234 | /// 235 | /// It is possible to inspect the `repr` field, if the need arises, but its 236 | /// precise format is an implementation detail and is subject to change. 237 | /// 238 | /// `Metadata` can be indexed by `PackageId`. 239 | #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] 240 | #[serde(transparent)] 241 | pub struct PackageId { 242 | /// The underlying string representation of id. 243 | pub repr: String, 244 | } 245 | 246 | impl fmt::Display for PackageId { 247 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 248 | fmt::Display::fmt(&self.repr, f) 249 | } 250 | } 251 | 252 | /// Helpers for default metadata fields 253 | fn is_null(value: &serde_json::Value) -> bool { 254 | matches!(value, serde_json::Value::Null) 255 | } 256 | 257 | #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)] 258 | #[cfg_attr(feature = "builder", derive(Builder))] 259 | #[non_exhaustive] 260 | #[cfg_attr(feature = "builder", builder(pattern = "owned", setter(into)))] 261 | /// Starting point for metadata returned by `cargo metadata` 262 | pub struct Metadata { 263 | /// A list of all crates referenced by this crate (and the crate itself) 264 | pub packages: Vec, 265 | /// A list of all workspace members 266 | pub workspace_members: Vec, 267 | /// The list of default workspace members 268 | /// 269 | /// This is not available if running with a version of Cargo older than 1.71. 270 | /// 271 | /// You can check whether it is available or missing using respectively 272 | /// [`WorkspaceDefaultMembers::is_available`] and [`WorkspaceDefaultMembers::is_missing`]. 273 | #[serde(default, skip_serializing_if = "WorkspaceDefaultMembers::is_missing")] 274 | pub workspace_default_members: WorkspaceDefaultMembers, 275 | /// Dependencies graph 276 | pub resolve: Option, 277 | /// Workspace root 278 | pub workspace_root: Utf8PathBuf, 279 | /// Target directory 280 | pub target_directory: Utf8PathBuf, 281 | /// Build directory 282 | // TODO: This should become non optional once the MSRV is at or above `1.91.0` 283 | pub build_directory: Option, 284 | /// The workspace-level metadata object. Null if non-existent. 285 | #[serde(rename = "metadata", default, skip_serializing_if = "is_null")] 286 | pub workspace_metadata: serde_json::Value, 287 | /// The metadata format version 288 | version: usize, 289 | } 290 | 291 | impl Metadata { 292 | /// Get the workspace's root package of this metadata instance. 293 | pub fn root_package(&self) -> Option<&Package> { 294 | match &self.resolve { 295 | Some(resolve) => { 296 | // if dependencies are resolved, use Cargo's answer 297 | let root = resolve.root.as_ref()?; 298 | self.packages.iter().find(|pkg| &pkg.id == root) 299 | } 300 | None => { 301 | // if dependencies aren't resolved, check for a root package manually 302 | let root_manifest_path = self.workspace_root.join("Cargo.toml"); 303 | self.packages 304 | .iter() 305 | .find(|pkg| pkg.manifest_path == root_manifest_path) 306 | } 307 | } 308 | } 309 | 310 | /// Get the workspace packages. 311 | pub fn workspace_packages(&self) -> Vec<&Package> { 312 | self.packages 313 | .iter() 314 | .filter(|&p| self.workspace_members.contains(&p.id)) 315 | .collect() 316 | } 317 | 318 | /// Get the workspace default packages. 319 | /// 320 | /// # Panics 321 | /// 322 | /// This will panic if running with a version of Cargo older than 1.71. 323 | pub fn workspace_default_packages(&self) -> Vec<&Package> { 324 | self.packages 325 | .iter() 326 | .filter(|&p| self.workspace_default_members.contains(&p.id)) 327 | .collect() 328 | } 329 | } 330 | 331 | impl<'a> std::ops::Index<&'a PackageId> for Metadata { 332 | type Output = Package; 333 | 334 | fn index(&self, idx: &'a PackageId) -> &Self::Output { 335 | self.packages 336 | .iter() 337 | .find(|p| p.id == *idx) 338 | .unwrap_or_else(|| panic!("no package with this id: {idx:?}")) 339 | } 340 | } 341 | 342 | #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash, Default)] 343 | #[serde(transparent)] 344 | /// A list of default workspace members. 345 | /// 346 | /// See [`Metadata::workspace_default_members`]. 347 | /// 348 | /// It is only available if running a version of Cargo of 1.71 or newer. 349 | /// 350 | /// # Panics 351 | /// 352 | /// Dereferencing when running an older version of Cargo will panic. 353 | pub struct WorkspaceDefaultMembers(Option>); 354 | 355 | impl WorkspaceDefaultMembers { 356 | /// Return `true` if the list of workspace default members is supported by 357 | /// the called cargo-metadata version and `false` otherwise. 358 | /// 359 | /// In particular useful when parsing the output of `cargo-metadata` for 360 | /// versions of Cargo < 1.71, as dereferencing [`WorkspaceDefaultMembers`] 361 | /// for these versions will panic. 362 | /// 363 | /// Opposite of [`WorkspaceDefaultMembers::is_missing`]. 364 | pub fn is_available(&self) -> bool { 365 | self.0.is_some() 366 | } 367 | 368 | /// Return `false` if the list of workspace default members is supported by 369 | /// the called cargo-metadata version and `true` otherwise. 370 | /// 371 | /// In particular useful when parsing the output of `cargo-metadata` for 372 | /// versions of Cargo < 1.71, as dereferencing [`WorkspaceDefaultMembers`] 373 | /// for these versions will panic. 374 | /// 375 | /// Opposite of [`WorkspaceDefaultMembers::is_available`]. 376 | pub fn is_missing(&self) -> bool { 377 | self.0.is_none() 378 | } 379 | } 380 | 381 | impl core::ops::Deref for WorkspaceDefaultMembers { 382 | type Target = [PackageId]; 383 | 384 | fn deref(&self) -> &Self::Target { 385 | self.0 386 | .as_ref() 387 | .expect("WorkspaceDefaultMembers should only be dereferenced on Cargo versions >= 1.71") 388 | } 389 | } 390 | 391 | #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)] 392 | #[cfg_attr(feature = "builder", derive(Builder))] 393 | #[non_exhaustive] 394 | #[cfg_attr(feature = "builder", builder(pattern = "owned", setter(into)))] 395 | /// A dependency graph 396 | pub struct Resolve { 397 | /// Nodes in a dependencies graph 398 | pub nodes: Vec, 399 | 400 | /// The crate for which the metadata was read. 401 | pub root: Option, 402 | } 403 | 404 | impl<'a> std::ops::Index<&'a PackageId> for Resolve { 405 | type Output = Node; 406 | 407 | fn index(&self, idx: &'a PackageId) -> &Self::Output { 408 | self.nodes 409 | .iter() 410 | .find(|p| p.id == *idx) 411 | .unwrap_or_else(|| panic!("no Node with this id: {idx:?}")) 412 | } 413 | } 414 | 415 | #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)] 416 | #[cfg_attr(feature = "builder", derive(Builder))] 417 | #[non_exhaustive] 418 | #[cfg_attr(feature = "builder", builder(pattern = "owned", setter(into)))] 419 | /// A node in a dependencies graph 420 | pub struct Node { 421 | /// An opaque identifier for a package 422 | pub id: PackageId, 423 | /// Dependencies in a structured format. 424 | /// 425 | /// `deps` handles renamed dependencies whereas `dependencies` does not. 426 | #[serde(default)] 427 | pub deps: Vec, 428 | 429 | /// List of opaque identifiers for this node's dependencies. 430 | /// It doesn't support renamed dependencies. See `deps`. 431 | pub dependencies: Vec, 432 | 433 | /// Features enabled on the crate 434 | #[serde(default)] 435 | pub features: Vec, 436 | } 437 | 438 | #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)] 439 | #[cfg_attr(feature = "builder", derive(Builder))] 440 | #[non_exhaustive] 441 | #[cfg_attr(feature = "builder", builder(pattern = "owned", setter(into)))] 442 | /// A dependency in a node 443 | pub struct NodeDep { 444 | /// The name of the dependency's library target. 445 | /// If the crate was renamed, it is the new name. 446 | /// 447 | /// If -Zbindeps is enabled local references may result in an empty 448 | /// string. 449 | /// 450 | /// After -Zbindeps gets stabilized, cargo has indicated this field 451 | /// will become deprecated. 452 | pub name: String, 453 | /// Package ID (opaque unique identifier) 454 | pub pkg: PackageId, 455 | /// The kinds of dependencies. 456 | /// 457 | /// This field was added in Rust 1.41. 458 | #[serde(default)] 459 | pub dep_kinds: Vec, 460 | } 461 | 462 | #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)] 463 | #[cfg_attr(feature = "builder", derive(Builder))] 464 | #[non_exhaustive] 465 | #[cfg_attr(feature = "builder", builder(pattern = "owned", setter(into)))] 466 | /// Information about a dependency kind. 467 | pub struct DepKindInfo { 468 | /// The kind of dependency. 469 | #[serde(deserialize_with = "dependency::parse_dependency_kind")] 470 | pub kind: DependencyKind, 471 | /// The target platform for the dependency. 472 | /// 473 | /// This is `None` if it is not a target dependency. 474 | /// 475 | /// Use the [`Display`] trait to access the contents. 476 | /// 477 | /// By default all platform dependencies are included in the resolve 478 | /// graph. Use Cargo's `--filter-platform` flag if you only want to 479 | /// include dependencies for a specific platform. 480 | /// 481 | /// [`Display`]: std::fmt::Display 482 | pub target: Option, 483 | } 484 | 485 | #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)] 486 | #[cfg_attr(feature = "builder", derive(Builder))] 487 | #[non_exhaustive] 488 | #[cfg_attr(feature = "builder", builder(pattern = "owned", setter(into)))] 489 | /// One or more crates described by a single `Cargo.toml` 490 | /// 491 | /// Each [`target`][Package::targets] of a `Package` will be built as a crate. 492 | /// For more information, see . 493 | pub struct Package { 494 | /// The [`name` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-name-field) as given in the `Cargo.toml` 495 | // (We say "given in" instead of "specified in" since the `name` key cannot be inherited from the workspace.) 496 | pub name: PackageName, 497 | /// The [`version` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-version-field) as specified in the `Cargo.toml` 498 | pub version: Version, 499 | /// The [`authors` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-authors-field) as specified in the `Cargo.toml` 500 | #[serde(default)] 501 | #[cfg_attr(feature = "builder", builder(default))] 502 | pub authors: Vec, 503 | /// An opaque identifier for a package 504 | pub id: PackageId, 505 | /// The source of the package, e.g. 506 | /// crates.io or `None` for local projects. 507 | // Note that this is NOT the same as cargo_util_schemas::RegistryName 508 | #[cfg_attr(feature = "builder", builder(default))] 509 | pub source: Option, 510 | /// The [`description` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-description-field) as specified in the `Cargo.toml` 511 | #[cfg_attr(feature = "builder", builder(default))] 512 | pub description: Option, 513 | /// List of dependencies of this particular package 514 | #[cfg_attr(feature = "builder", builder(default))] 515 | pub dependencies: Vec, 516 | /// The [`license` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-license-and-license-file-fields) as specified in the `Cargo.toml` 517 | #[cfg_attr(feature = "builder", builder(default))] 518 | pub license: Option, 519 | /// The [`license-file` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-license-and-license-file-fields) as specified in the `Cargo.toml`. 520 | /// If the package is using a nonstandard license, this key may be specified instead of 521 | /// `license`, and must point to a file relative to the manifest. 522 | #[cfg_attr(feature = "builder", builder(default))] 523 | pub license_file: Option, 524 | /// Targets provided by the crate (lib, bin, example, test, ...) 525 | #[cfg_attr(feature = "builder", builder(default))] 526 | pub targets: Vec, 527 | /// Features provided by the crate, mapped to the features required by that feature. 528 | #[cfg_attr(feature = "builder", builder(default))] 529 | pub features: BTreeMap>, 530 | /// Path containing the `Cargo.toml` 531 | pub manifest_path: Utf8PathBuf, 532 | /// The [`categories` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-categories-field) as specified in the `Cargo.toml` 533 | #[serde(default)] 534 | #[cfg_attr(feature = "builder", builder(default))] 535 | pub categories: Vec, 536 | /// The [`keywords` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-keywords-field) as specified in the `Cargo.toml` 537 | #[serde(default)] 538 | #[cfg_attr(feature = "builder", builder(default))] 539 | pub keywords: Vec, 540 | /// The [`readme` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-readme-field) as specified in the `Cargo.toml` 541 | #[cfg_attr(feature = "builder", builder(default))] 542 | pub readme: Option, 543 | /// The [`repository` URL](https://doc.rust-lang.org/cargo/reference/manifest.html#the-repository-field) as specified in the `Cargo.toml` 544 | // can't use `url::Url` because that requires a more recent stable compiler 545 | #[cfg_attr(feature = "builder", builder(default))] 546 | pub repository: Option, 547 | /// The [`homepage` URL](https://doc.rust-lang.org/cargo/reference/manifest.html#the-homepage-field) as specified in the `Cargo.toml`. 548 | /// 549 | /// On versions of cargo before 1.49, this will always be [`None`]. 550 | #[cfg_attr(feature = "builder", builder(default))] 551 | pub homepage: Option, 552 | /// The [`documentation` URL](https://doc.rust-lang.org/cargo/reference/manifest.html#the-documentation-field) as specified in the `Cargo.toml`. 553 | /// 554 | /// On versions of cargo before 1.49, this will always be [`None`]. 555 | #[cfg_attr(feature = "builder", builder(default))] 556 | pub documentation: Option, 557 | /// The default Rust edition for the package (either what's specified in the [`edition` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-edition-field) 558 | /// or defaulting to [`Edition::E2015`]). 559 | /// 560 | /// Beware that individual targets may specify their own edition in 561 | /// [`Target::edition`]. 562 | #[serde(default)] 563 | #[cfg_attr(feature = "builder", builder(default))] 564 | pub edition: Edition, 565 | /// Contents of the free form [`package.metadata` section](https://doc.rust-lang.org/cargo/reference/manifest.html#the-metadata-table). 566 | /// 567 | /// This contents can be serialized to a struct using serde: 568 | /// 569 | /// ```rust 570 | /// use serde::Deserialize; 571 | /// use serde_json::json; 572 | /// 573 | /// #[derive(Debug, Deserialize)] 574 | /// struct SomePackageMetadata { 575 | /// some_value: i32, 576 | /// } 577 | /// 578 | /// let value = json!({ 579 | /// "some_value": 42, 580 | /// }); 581 | /// 582 | /// let package_metadata: SomePackageMetadata = serde_json::from_value(value).unwrap(); 583 | /// assert_eq!(package_metadata.some_value, 42); 584 | /// 585 | /// ``` 586 | #[serde(default, skip_serializing_if = "is_null")] 587 | #[cfg_attr(feature = "builder", builder(default))] 588 | pub metadata: serde_json::Value, 589 | /// The name of a native library the package is linking to. 590 | #[cfg_attr(feature = "builder", builder(default))] 591 | pub links: Option, 592 | /// List of registries to which this package may be published (derived from the [`publish` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field)). 593 | /// 594 | /// Publishing is unrestricted if `None`, and forbidden if the `Vec` is empty. 595 | /// 596 | /// This is always `None` if running with a version of Cargo older than 1.39. 597 | #[cfg_attr(feature = "builder", builder(default))] 598 | pub publish: Option>, 599 | /// The [`default-run` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-default-run-field) as given in the `Cargo.toml` 600 | // (We say "given in" instead of "specified in" since the `default-run` key cannot be inherited from the workspace.) 601 | /// The default binary to run by `cargo run`. 602 | /// 603 | /// This is always `None` if running with a version of Cargo older than 1.55. 604 | #[cfg_attr(feature = "builder", builder(default))] 605 | pub default_run: Option, 606 | /// The [`rust-version` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-rust-version-field) as specified in the `Cargo.toml`. 607 | /// The minimum supported Rust version of this package. 608 | /// 609 | /// This is always `None` if running with a version of Cargo older than 1.58. 610 | #[serde(default)] 611 | #[serde(deserialize_with = "deserialize_rust_version")] 612 | #[cfg_attr(feature = "builder", builder(default))] 613 | pub rust_version: Option, 614 | } 615 | 616 | #[cfg(feature = "builder")] 617 | impl PackageBuilder { 618 | /// Construct a new `PackageBuilder` with all required fields. 619 | pub fn new( 620 | name: impl Into, 621 | version: impl Into, 622 | id: impl Into, 623 | path: impl Into, 624 | ) -> Self { 625 | Self::default() 626 | .name(name) 627 | .version(version) 628 | .id(id) 629 | .manifest_path(path) 630 | } 631 | } 632 | 633 | impl Package { 634 | /// Full path to the license file if one is present in the manifest 635 | pub fn license_file(&self) -> Option { 636 | self.license_file.as_ref().map(|file| { 637 | self.manifest_path 638 | .parent() 639 | .unwrap_or(&self.manifest_path) 640 | .join(file) 641 | }) 642 | } 643 | 644 | /// Full path to the readme file if one is present in the manifest 645 | pub fn readme(&self) -> Option { 646 | self.readme.as_ref().map(|file| { 647 | self.manifest_path 648 | .parent() 649 | .unwrap_or(&self.manifest_path) 650 | .join(file) 651 | }) 652 | } 653 | } 654 | 655 | /// The source of a package such as crates.io. 656 | /// 657 | /// It is possible to inspect the `repr` field, if the need arises, but its 658 | /// precise format is an implementation detail and is subject to change. 659 | #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)] 660 | #[serde(transparent)] 661 | pub struct Source { 662 | /// The underlying string representation of a source. 663 | pub repr: String, 664 | } 665 | 666 | impl Source { 667 | /// Returns true if the source is crates.io. 668 | pub fn is_crates_io(&self) -> bool { 669 | self.repr == "registry+https://github.com/rust-lang/crates.io-index" 670 | } 671 | } 672 | 673 | impl fmt::Display for Source { 674 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 675 | fmt::Display::fmt(&self.repr, f) 676 | } 677 | } 678 | 679 | #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)] 680 | #[cfg_attr(feature = "builder", derive(Builder))] 681 | #[cfg_attr(feature = "builder", builder(pattern = "owned", setter(into)))] 682 | #[non_exhaustive] 683 | /// A single target (lib, bin, example, ...) provided by a crate 684 | pub struct Target { 685 | /// Name as given in the `Cargo.toml` or generated from the file name 686 | pub name: String, 687 | /// Kind of target. 688 | /// 689 | /// The possible values are `example`, `test`, `bench`, `custom-build` and 690 | /// [Cargo crate types](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-crate-type-field): 691 | /// `bin`, `lib`, `rlib`, `dylib`, `cdylib`, `staticlib`, `proc-macro`. 692 | /// 693 | /// Other possible values may be added in the future. 694 | pub kind: Vec, 695 | /// Similar to `kind`, but only reports the 696 | /// [Cargo crate types](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-crate-type-field): 697 | /// `bin`, `lib`, `rlib`, `dylib`, `cdylib`, `staticlib`, `proc-macro`. 698 | /// Everything that's not a proc macro or a library of some kind is reported as "bin". 699 | /// 700 | /// Other possible values may be added in the future. 701 | #[serde(default)] 702 | #[cfg_attr(feature = "builder", builder(default))] 703 | pub crate_types: Vec, 704 | 705 | #[serde(default)] 706 | #[cfg_attr(feature = "builder", builder(default))] 707 | #[serde(rename = "required-features")] 708 | /// This target is built only if these features are enabled. 709 | /// It doesn't apply to `lib` targets. 710 | pub required_features: Vec, 711 | /// Path to the main source file of the target 712 | pub src_path: Utf8PathBuf, 713 | /// Rust edition for this target 714 | #[serde(default)] 715 | #[cfg_attr(feature = "builder", builder(default))] 716 | pub edition: Edition, 717 | /// Whether or not this target has doc tests enabled, and the target is 718 | /// compatible with doc testing. 719 | /// 720 | /// This is always `true` if running with a version of Cargo older than 1.37. 721 | #[serde(default = "default_true")] 722 | #[cfg_attr(feature = "builder", builder(default = "true"))] 723 | pub doctest: bool, 724 | /// Whether or not this target is tested by default by `cargo test`. 725 | /// 726 | /// This is always `true` if running with a version of Cargo older than 1.47. 727 | #[serde(default = "default_true")] 728 | #[cfg_attr(feature = "builder", builder(default = "true"))] 729 | pub test: bool, 730 | /// Whether or not this target is documented by `cargo doc`. 731 | /// 732 | /// This is always `true` if running with a version of Cargo older than 1.50. 733 | #[serde(default = "default_true")] 734 | #[cfg_attr(feature = "builder", builder(default = "true"))] 735 | pub doc: bool, 736 | } 737 | 738 | macro_rules! methods_target_is_kind { 739 | ($($name:ident => $kind:expr),*) => { 740 | $( 741 | /// Return true if this target is of kind `$kind`. 742 | pub fn $name(&self) -> bool { 743 | self.is_kind($kind) 744 | } 745 | )* 746 | } 747 | } 748 | 749 | impl Target { 750 | /// Return true if this target is of the given kind. 751 | pub fn is_kind(&self, name: TargetKind) -> bool { 752 | self.kind.iter().any(|kind| kind == &name) 753 | } 754 | 755 | // Generate `is_*` methods for each `TargetKind` 756 | methods_target_is_kind! { 757 | is_lib => TargetKind::Lib, 758 | is_bin => TargetKind::Bin, 759 | is_example => TargetKind::Example, 760 | is_test => TargetKind::Test, 761 | is_bench => TargetKind::Bench, 762 | is_custom_build => TargetKind::CustomBuild, 763 | is_proc_macro => TargetKind::ProcMacro, 764 | is_cdylib => TargetKind::CDyLib, 765 | is_dylib => TargetKind::DyLib, 766 | is_rlib => TargetKind::RLib, 767 | is_staticlib => TargetKind::StaticLib 768 | } 769 | } 770 | 771 | /// Kind of target. 772 | /// 773 | /// The possible values are `example`, `test`, `bench`, `custom-build` and 774 | /// [Cargo crate types](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-crate-type-field): 775 | /// `bin`, `lib`, `rlib`, `dylib`, `cdylib`, `staticlib`, `proc-macro`. 776 | /// 777 | /// Other possible values may be added in the future. 778 | #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] 779 | #[non_exhaustive] 780 | pub enum TargetKind { 781 | /// `cargo bench` target 782 | #[serde(rename = "bench")] 783 | Bench, 784 | /// Binary executable target 785 | #[serde(rename = "bin")] 786 | Bin, 787 | /// Custom build target 788 | #[serde(rename = "custom-build")] 789 | CustomBuild, 790 | /// Dynamic system library target 791 | #[serde(rename = "cdylib")] 792 | CDyLib, 793 | /// Dynamic Rust library target 794 | #[serde(rename = "dylib")] 795 | DyLib, 796 | /// Example target 797 | #[serde(rename = "example")] 798 | Example, 799 | /// Rust library 800 | #[serde(rename = "lib")] 801 | Lib, 802 | /// Procedural Macro 803 | #[serde(rename = "proc-macro")] 804 | ProcMacro, 805 | /// Rust library for use as an intermediate artifact 806 | #[serde(rename = "rlib")] 807 | RLib, 808 | /// Static system library 809 | #[serde(rename = "staticlib")] 810 | StaticLib, 811 | /// Test target 812 | #[serde(rename = "test")] 813 | Test, 814 | /// Unknown type 815 | #[serde(untagged)] 816 | Unknown(String), 817 | } 818 | 819 | impl From<&str> for TargetKind { 820 | fn from(value: &str) -> Self { 821 | match value { 822 | "example" => TargetKind::Example, 823 | "test" => TargetKind::Test, 824 | "bench" => TargetKind::Bench, 825 | "custom-build" => TargetKind::CustomBuild, 826 | "bin" => TargetKind::Bin, 827 | "lib" => TargetKind::Lib, 828 | "rlib" => TargetKind::RLib, 829 | "dylib" => TargetKind::DyLib, 830 | "cdylib" => TargetKind::CDyLib, 831 | "staticlib" => TargetKind::StaticLib, 832 | "proc-macro" => TargetKind::ProcMacro, 833 | x => TargetKind::Unknown(x.to_string()), 834 | } 835 | } 836 | } 837 | 838 | impl FromStr for TargetKind { 839 | type Err = std::convert::Infallible; 840 | 841 | fn from_str(s: &str) -> std::result::Result { 842 | Ok(TargetKind::from(s)) 843 | } 844 | } 845 | 846 | impl fmt::Display for TargetKind { 847 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 848 | match self { 849 | Self::Bench => "bench".fmt(f), 850 | Self::Bin => "bin".fmt(f), 851 | Self::CustomBuild => "custom-build".fmt(f), 852 | Self::CDyLib => "cdylib".fmt(f), 853 | Self::DyLib => "dylib".fmt(f), 854 | Self::Example => "example".fmt(f), 855 | Self::Lib => "lib".fmt(f), 856 | Self::ProcMacro => "proc-macro".fmt(f), 857 | Self::RLib => "rlib".fmt(f), 858 | Self::StaticLib => "staticlib".fmt(f), 859 | Self::Test => "test".fmt(f), 860 | Self::Unknown(x) => x.fmt(f), 861 | } 862 | } 863 | } 864 | 865 | /// Similar to `kind`, but only reports the 866 | /// [Cargo crate types](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-crate-type-field): 867 | /// 868 | /// `bin`, `lib`, `rlib`, `dylib`, `cdylib`, `staticlib`, `proc-macro`. 869 | /// Everything that's not a proc macro or a library of some kind is reported as "bin". 870 | /// 871 | /// Other possible values may be added in the future. 872 | #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] 873 | #[non_exhaustive] 874 | pub enum CrateType { 875 | /// Binary executable target 876 | #[serde(rename = "bin")] 877 | Bin, 878 | /// Dynamic system library target 879 | #[serde(rename = "cdylib")] 880 | CDyLib, 881 | /// Dynamic Rust library target 882 | #[serde(rename = "dylib")] 883 | DyLib, 884 | /// Rust library 885 | #[serde(rename = "lib")] 886 | Lib, 887 | /// Procedural Macro 888 | #[serde(rename = "proc-macro")] 889 | ProcMacro, 890 | /// Rust library for use as an intermediate artifact 891 | #[serde(rename = "rlib")] 892 | RLib, 893 | /// Static system library 894 | #[serde(rename = "staticlib")] 895 | StaticLib, 896 | /// Unkown type 897 | #[serde(untagged)] 898 | Unknown(String), 899 | } 900 | 901 | impl From<&str> for CrateType { 902 | fn from(value: &str) -> Self { 903 | match value { 904 | "bin" => CrateType::Bin, 905 | "lib" => CrateType::Lib, 906 | "rlib" => CrateType::RLib, 907 | "dylib" => CrateType::DyLib, 908 | "cdylib" => CrateType::CDyLib, 909 | "staticlib" => CrateType::StaticLib, 910 | "proc-macro" => CrateType::ProcMacro, 911 | x => CrateType::Unknown(x.to_string()), 912 | } 913 | } 914 | } 915 | 916 | impl FromStr for CrateType { 917 | type Err = std::convert::Infallible; 918 | 919 | fn from_str(s: &str) -> std::result::Result { 920 | Ok(CrateType::from(s)) 921 | } 922 | } 923 | 924 | impl fmt::Display for CrateType { 925 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 926 | match self { 927 | Self::Bin => "bin".fmt(f), 928 | Self::CDyLib => "cdylib".fmt(f), 929 | Self::DyLib => "dylib".fmt(f), 930 | Self::Lib => "lib".fmt(f), 931 | Self::ProcMacro => "proc-macro".fmt(f), 932 | Self::RLib => "rlib".fmt(f), 933 | Self::StaticLib => "staticlib".fmt(f), 934 | Self::Unknown(x) => x.fmt(f), 935 | } 936 | } 937 | } 938 | 939 | /// The Rust edition 940 | /// 941 | /// As of writing this comment rust editions 2027 and 2030 are not actually a thing yet but are parsed nonetheless for future proofing. 942 | #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)] 943 | #[non_exhaustive] 944 | #[derive(Default)] 945 | pub enum Edition { 946 | /// Edition 2015 947 | #[serde(rename = "2015")] 948 | #[default] 949 | E2015, 950 | /// Edition 2018 951 | #[serde(rename = "2018")] 952 | E2018, 953 | /// Edition 2021 954 | #[serde(rename = "2021")] 955 | E2021, 956 | /// Edition 2024 957 | #[serde(rename = "2024")] 958 | E2024, 959 | #[doc(hidden)] 960 | #[serde(rename = "2027")] 961 | _E2027, 962 | #[doc(hidden)] 963 | #[serde(rename = "2030")] 964 | _E2030, 965 | } 966 | 967 | impl Edition { 968 | /// Return the string representation of the edition 969 | pub fn as_str(&self) -> &'static str { 970 | use Edition::*; 971 | match self { 972 | E2015 => "2015", 973 | E2018 => "2018", 974 | E2021 => "2021", 975 | E2024 => "2024", 976 | _E2027 => "2027", 977 | _E2030 => "2030", 978 | } 979 | } 980 | } 981 | 982 | impl fmt::Display for Edition { 983 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 984 | f.write_str(self.as_str()) 985 | } 986 | } 987 | 988 | fn default_true() -> bool { 989 | true 990 | } 991 | 992 | /// Cargo features flags 993 | #[derive(Debug, Clone)] 994 | pub enum CargoOpt { 995 | /// Run cargo with `--features-all` 996 | AllFeatures, 997 | /// Run cargo with `--no-default-features` 998 | NoDefaultFeatures, 999 | /// Run cargo with `--features ` 1000 | SomeFeatures(Vec), 1001 | } 1002 | 1003 | /// A builder for configuring `cargo metadata` invocation. 1004 | #[derive(Debug, Clone, Default)] 1005 | pub struct MetadataCommand { 1006 | /// Path to `cargo` executable. If not set, this will use the 1007 | /// the `$CARGO` environment variable, and if that is not set, will 1008 | /// simply be `cargo`. 1009 | cargo_path: Option, 1010 | /// Path to `Cargo.toml` 1011 | manifest_path: Option, 1012 | /// Current directory of the `cargo metadata` process. 1013 | current_dir: Option, 1014 | /// Output information only about workspace members and don't fetch dependencies. 1015 | no_deps: bool, 1016 | /// Collections of `CargoOpt::SomeFeatures(..)` 1017 | features: Vec, 1018 | /// Latched `CargoOpt::AllFeatures` 1019 | all_features: bool, 1020 | /// Latched `CargoOpt::NoDefaultFeatures` 1021 | no_default_features: bool, 1022 | /// Arbitrary command line flags to pass to `cargo`. These will be added 1023 | /// to the end of the command line invocation. 1024 | other_options: Vec, 1025 | /// Arbitrary environment variables to set or remove (depending on 1026 | /// [`Option`] value) when running `cargo`. These will be merged into the 1027 | /// calling environment, overriding any which clash. 1028 | env: BTreeMap>, 1029 | /// Show stderr 1030 | verbose: bool, 1031 | } 1032 | 1033 | impl MetadataCommand { 1034 | /// Creates a default `cargo metadata` command, which will look for 1035 | /// `Cargo.toml` in the ancestors of the current directory. 1036 | pub fn new() -> MetadataCommand { 1037 | MetadataCommand::default() 1038 | } 1039 | /// Path to `cargo` executable. If not set, this will use the 1040 | /// the `$CARGO` environment variable, and if that is not set, will 1041 | /// simply be `cargo`. 1042 | pub fn cargo_path(&mut self, path: impl Into) -> &mut MetadataCommand { 1043 | self.cargo_path = Some(path.into()); 1044 | self 1045 | } 1046 | /// Path to `Cargo.toml` 1047 | pub fn manifest_path(&mut self, path: impl Into) -> &mut MetadataCommand { 1048 | self.manifest_path = Some(path.into()); 1049 | self 1050 | } 1051 | /// Current directory of the `cargo metadata` process. 1052 | pub fn current_dir(&mut self, path: impl Into) -> &mut MetadataCommand { 1053 | self.current_dir = Some(path.into()); 1054 | self 1055 | } 1056 | /// Output information only about workspace members and don't fetch dependencies. 1057 | pub fn no_deps(&mut self) -> &mut MetadataCommand { 1058 | self.no_deps = true; 1059 | self 1060 | } 1061 | /// Which features to include. 1062 | /// 1063 | /// Call this multiple times to specify advanced feature configurations: 1064 | /// 1065 | /// ```no_run 1066 | /// # use cargo_metadata::{CargoOpt, MetadataCommand}; 1067 | /// MetadataCommand::new() 1068 | /// .features(CargoOpt::NoDefaultFeatures) 1069 | /// .features(CargoOpt::SomeFeatures(vec!["feat1".into(), "feat2".into()])) 1070 | /// .features(CargoOpt::SomeFeatures(vec!["feat3".into()])) 1071 | /// // ... 1072 | /// # ; 1073 | /// ``` 1074 | /// 1075 | /// # Panics 1076 | /// 1077 | /// `cargo metadata` rejects multiple `--no-default-features` flags. Similarly, the `features()` 1078 | /// method panics when specifying multiple `CargoOpt::NoDefaultFeatures`: 1079 | /// 1080 | /// ```should_panic 1081 | /// # use cargo_metadata::{CargoOpt, MetadataCommand}; 1082 | /// MetadataCommand::new() 1083 | /// .features(CargoOpt::NoDefaultFeatures) 1084 | /// .features(CargoOpt::NoDefaultFeatures) // <-- panic! 1085 | /// // ... 1086 | /// # ; 1087 | /// ``` 1088 | /// 1089 | /// The method also panics for multiple `CargoOpt::AllFeatures` arguments: 1090 | /// 1091 | /// ```should_panic 1092 | /// # use cargo_metadata::{CargoOpt, MetadataCommand}; 1093 | /// MetadataCommand::new() 1094 | /// .features(CargoOpt::AllFeatures) 1095 | /// .features(CargoOpt::AllFeatures) // <-- panic! 1096 | /// // ... 1097 | /// # ; 1098 | /// ``` 1099 | pub fn features(&mut self, features: CargoOpt) -> &mut MetadataCommand { 1100 | match features { 1101 | CargoOpt::SomeFeatures(features) => self.features.extend(features), 1102 | CargoOpt::NoDefaultFeatures => { 1103 | assert!( 1104 | !self.no_default_features, 1105 | "Do not supply CargoOpt::NoDefaultFeatures more than once!" 1106 | ); 1107 | self.no_default_features = true; 1108 | } 1109 | CargoOpt::AllFeatures => { 1110 | assert!( 1111 | !self.all_features, 1112 | "Do not supply CargoOpt::AllFeatures more than once!" 1113 | ); 1114 | self.all_features = true; 1115 | } 1116 | } 1117 | self 1118 | } 1119 | /// Arbitrary command line flags to pass to `cargo`. These will be added 1120 | /// to the end of the command line invocation. 1121 | pub fn other_options(&mut self, options: impl Into>) -> &mut MetadataCommand { 1122 | self.other_options = options.into(); 1123 | self 1124 | } 1125 | 1126 | /// Arbitrary environment variables to set when running `cargo`. These will be merged into 1127 | /// the calling environment, overriding any which clash. 1128 | /// 1129 | /// Some examples of when you may want to use this: 1130 | /// 1. Setting cargo config values without needing a .cargo/config.toml file, e.g. to set 1131 | /// `CARGO_NET_GIT_FETCH_WITH_CLI=true` 1132 | /// 2. To specify a custom path to RUSTC if your rust toolchain components aren't laid out in 1133 | /// the way cargo expects by default. 1134 | /// 1135 | /// ```no_run 1136 | /// # use cargo_metadata::{CargoOpt, MetadataCommand}; 1137 | /// MetadataCommand::new() 1138 | /// .env("CARGO_NET_GIT_FETCH_WITH_CLI", "true") 1139 | /// .env("RUSTC", "/path/to/rustc") 1140 | /// // ... 1141 | /// # ; 1142 | /// ``` 1143 | pub fn env, V: Into>( 1144 | &mut self, 1145 | key: K, 1146 | val: V, 1147 | ) -> &mut MetadataCommand { 1148 | self.env.insert(key.into(), Some(val.into())); 1149 | self 1150 | } 1151 | 1152 | /// Arbitrary environment variables to remove when running `cargo`. These will be merged into 1153 | /// the calling environment, overriding any which clash. 1154 | /// 1155 | /// Some examples of when you may want to use this: 1156 | /// - Removing inherited environment variables in build scripts that can cause an error 1157 | /// when calling `cargo metadata` (for example, when cross-compiling). 1158 | /// 1159 | /// ```no_run 1160 | /// # use cargo_metadata::{CargoOpt, MetadataCommand}; 1161 | /// MetadataCommand::new() 1162 | /// .env_remove("CARGO_ENCODED_RUSTFLAGS") 1163 | /// // ... 1164 | /// # ; 1165 | /// ``` 1166 | pub fn env_remove>(&mut self, key: K) -> &mut MetadataCommand { 1167 | self.env.insert(key.into(), None); 1168 | self 1169 | } 1170 | 1171 | /// Set whether to show stderr 1172 | pub fn verbose(&mut self, verbose: bool) -> &mut MetadataCommand { 1173 | self.verbose = verbose; 1174 | self 1175 | } 1176 | 1177 | /// Builds a command for `cargo metadata`. This is the first 1178 | /// part of the work of `exec`. 1179 | pub fn cargo_command(&self) -> Command { 1180 | let cargo = self 1181 | .cargo_path 1182 | .clone() 1183 | .or_else(|| env::var("CARGO").map(PathBuf::from).ok()) 1184 | .unwrap_or_else(|| PathBuf::from("cargo")); 1185 | let mut cmd = Command::new(cargo); 1186 | cmd.args(["metadata", "--format-version", "1"]); 1187 | 1188 | if self.no_deps { 1189 | cmd.arg("--no-deps"); 1190 | } 1191 | 1192 | if let Some(path) = self.current_dir.as_ref() { 1193 | cmd.current_dir(path); 1194 | } 1195 | 1196 | if !self.features.is_empty() { 1197 | cmd.arg("--features").arg(self.features.join(",")); 1198 | } 1199 | if self.all_features { 1200 | cmd.arg("--all-features"); 1201 | } 1202 | if self.no_default_features { 1203 | cmd.arg("--no-default-features"); 1204 | } 1205 | 1206 | if let Some(manifest_path) = &self.manifest_path { 1207 | cmd.arg("--manifest-path").arg(manifest_path.as_os_str()); 1208 | } 1209 | cmd.args(&self.other_options); 1210 | 1211 | for (key, val) in &self.env { 1212 | match val { 1213 | Some(val) => cmd.env(key, val), 1214 | None => cmd.env_remove(key), 1215 | }; 1216 | } 1217 | 1218 | cmd 1219 | } 1220 | 1221 | /// Parses `cargo metadata` output. `data` must have been 1222 | /// produced by a command built with `cargo_command`. 1223 | pub fn parse>(data: T) -> Result { 1224 | let meta = serde_json::from_str(data.as_ref())?; 1225 | Ok(meta) 1226 | } 1227 | 1228 | /// Runs configured `cargo metadata` and returns parsed `Metadata`. 1229 | pub fn exec(&self) -> Result { 1230 | let mut command = self.cargo_command(); 1231 | if self.verbose { 1232 | command.stderr(Stdio::inherit()); 1233 | } 1234 | let output = command.output()?; 1235 | if !output.status.success() { 1236 | return Err(Error::CargoMetadata { 1237 | stderr: String::from_utf8(output.stderr)?, 1238 | }); 1239 | } 1240 | let stdout = from_utf8(&output.stdout)? 1241 | .lines() 1242 | .find(|line| line.starts_with('{')) 1243 | .ok_or(Error::NoJson)?; 1244 | Self::parse(stdout) 1245 | } 1246 | } 1247 | 1248 | /// As per the Cargo Book the [`rust-version` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-rust-version-field) must: 1249 | /// 1250 | /// > be a bare version number with two or three components; 1251 | /// > it cannot include semver operators or pre-release identifiers. 1252 | /// 1253 | /// [`semver::Version`] however requires three components. This function takes 1254 | /// care of appending `.0` if the provided version number only has two components 1255 | /// and ensuring that it does not contain a pre-release version or build metadata. 1256 | fn deserialize_rust_version<'de, D>( 1257 | deserializer: D, 1258 | ) -> std::result::Result, D::Error> 1259 | where 1260 | D: Deserializer<'de>, 1261 | { 1262 | let mut buf = match Option::::deserialize(deserializer)? { 1263 | None => return Ok(None), 1264 | Some(buf) => buf, 1265 | }; 1266 | 1267 | for char in buf.chars() { 1268 | if char == '-' { 1269 | return Err(serde::de::Error::custom( 1270 | "pre-release identifiers are not supported in rust-version", 1271 | )); 1272 | } else if char == '+' { 1273 | return Err(serde::de::Error::custom( 1274 | "build metadata is not supported in rust-version", 1275 | )); 1276 | } 1277 | } 1278 | 1279 | if buf.matches('.').count() == 1 { 1280 | // e.g. 1.0 -> 1.0.0 1281 | buf.push_str(".0"); 1282 | } 1283 | 1284 | Ok(Some( 1285 | Version::parse(&buf).map_err(serde::de::Error::custom)?, 1286 | )) 1287 | } 1288 | 1289 | #[cfg(test)] 1290 | mod test { 1291 | use semver::Version; 1292 | 1293 | #[derive(Debug, serde::Deserialize)] 1294 | struct BareVersion( 1295 | #[serde(deserialize_with = "super::deserialize_rust_version")] Option, 1296 | ); 1297 | 1298 | fn bare_version(str: &str) -> Version { 1299 | serde_json::from_str::(&format!(r#""{}""#, str)) 1300 | .unwrap() 1301 | .0 1302 | .unwrap() 1303 | } 1304 | 1305 | fn bare_version_err(str: &str) -> String { 1306 | serde_json::from_str::(&format!(r#""{}""#, str)) 1307 | .unwrap_err() 1308 | .to_string() 1309 | } 1310 | 1311 | #[test] 1312 | fn test_deserialize_rust_version() { 1313 | assert_eq!(bare_version("1.2"), Version::new(1, 2, 0)); 1314 | assert_eq!(bare_version("1.2.0"), Version::new(1, 2, 0)); 1315 | assert_eq!( 1316 | bare_version_err("1.2.0-alpha"), 1317 | "pre-release identifiers are not supported in rust-version" 1318 | ); 1319 | assert_eq!( 1320 | bare_version_err("1.2.0+123"), 1321 | "build metadata is not supported in rust-version" 1322 | ); 1323 | } 1324 | 1325 | #[test] 1326 | fn package_name_eq() { 1327 | let my_package_name = super::PackageName::new("my_package"); 1328 | assert_eq!(my_package_name, "my_package"); 1329 | } 1330 | } 1331 | --------------------------------------------------------------------------------