From a34963dbeb3196f3be68ca961f5b586986bd17ec Mon Sep 17 00:00:00 2001 From: Mathias H Date: Thu, 8 Aug 2024 22:46:53 +0200 Subject: [PATCH] initial commit --- .github/workflows/ci.yaml | 28 + .github/workflows/release-plz.yml | 27 + .gitignore | 9 + CHANGELOG.md | 12 + Cargo.toml | 32 ++ LICENSE | 21 + README.md | 68 +++ UNLICENSE | 24 + examples/basic.rs | 95 ++++ examples/cpp/CMakeLists.txt | 5 + examples/cpp/main.cpp | 5 + examples/example_from_readme.rs | 34 ++ src/index.rs | 502 +++++++++++++++++ src/lib.rs | 69 +++ src/objects.rs | 71 +++ src/objects/cache_v2.rs | 125 +++++ src/objects/cmake_files_v1.rs | 136 +++++ src/objects/codemodel_v2.rs | 9 + src/objects/codemodel_v2/backtrace_graph.rs | 112 ++++ src/objects/codemodel_v2/codemodel.rs | 316 +++++++++++ src/objects/codemodel_v2/directory.rs | 224 ++++++++ src/objects/codemodel_v2/target.rs | 567 ++++++++++++++++++++ src/objects/configure_log_v1.rs | 74 +++ src/objects/toolchains_v1.rs | 184 +++++++ src/query.rs | 176 ++++++ src/reply.rs | 170 ++++++ tests/test_query.rs | 90 ++++ tests/test_real_projects.rs | 191 +++++++ tests/test_reply.rs | 178 ++++++ 29 files changed, 3554 insertions(+) create mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/release-plz.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 UNLICENSE create mode 100644 examples/basic.rs create mode 100644 examples/cpp/CMakeLists.txt create mode 100644 examples/cpp/main.cpp create mode 100644 examples/example_from_readme.rs create mode 100644 src/index.rs create mode 100644 src/lib.rs create mode 100644 src/objects.rs create mode 100644 src/objects/cache_v2.rs create mode 100644 src/objects/cmake_files_v1.rs create mode 100644 src/objects/codemodel_v2.rs create mode 100644 src/objects/codemodel_v2/backtrace_graph.rs create mode 100644 src/objects/codemodel_v2/codemodel.rs create mode 100644 src/objects/codemodel_v2/directory.rs create mode 100644 src/objects/codemodel_v2/target.rs create mode 100644 src/objects/configure_log_v1.rs create mode 100644 src/objects/toolchains_v1.rs create mode 100644 src/query.rs create mode 100644 src/reply.rs create mode 100644 tests/test_query.rs create mode 100644 tests/test_real_projects.rs create mode 100644 tests/test_reply.rs diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..44e4f8f --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,28 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: Swatinem/rust-cache@v2 + - name: clippy + run: cargo clippy + - name: Install LLVM and Clang + uses: KyleMayes/install-llvm-action@v2 + with: + version: "17.0" + - name: Install cmake + ninja + run: sudo apt-get install -y cmake ninja-build + - name: tests + run: cargo test + - name: slow tests + run: cargo test -- --ignored + - name: docs + run: cargo doc \ No newline at end of file diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml new file mode 100644 index 0000000..85bd88f --- /dev/null +++ b/.github/workflows/release-plz.yml @@ -0,0 +1,27 @@ +name: Release-plz + +permissions: + pull-requests: write + contents: write + +on: + push: + branches: + - main + +jobs: + release-plz: + name: Release-plz + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Run release-plz + uses: MarcoIeni/release-plz-action@v0.5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3158c5a --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +Cargo.lock + +# CMake build directory created by running the basic example without args +examples/cpp/build/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..85c1628 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.0.9](https://github.com/h-mathias/cmake-file-api-rs/releases/tag/v0.0.9) - 2024-08-08 + +### Other +- Initial commit diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8c046bf --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "cmake-file-api" +version = "0.1.0" +authors = ["Mathias H. "] +description = "Parsing and interacting with cmake-file-api" +homepage = "https://github.com/h-mathias/cmake-file-api-rs" +repository = "https://github.com/h-mathias/cmake-file-api-rs" +readme = "README.md" +keywords = ["cmake", "cmake-file-api", "cpp"] +categories = ["api-bindings", "filesystem"] +license = "MIT" +edition = "2021" + +[dependencies] +serde_json = "1.0" +serde = { version = "1.0", features = ["derive"] } +thiserror = "1.0" +shlex = "1.3" + +[dev-dependencies] +tempdir = "0.3" + +[lints.rust] +unsafe_code = "forbid" + +[lints.clippy] +shadow_reuse = "forbid" +exhaustive_enums = "forbid" +panic = "forbid" +shadow_unrelated = "forbid" + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7e4a1a4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 h-mathias + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b16507f --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +![GitHub Workflow Status](https://github.com/h-mathias/cmake-file-api-rs/actions/workflows/ci.yaml/badge.svg) + +cmake-file-api-rs +======= + +Library for interacting with the [cmake-file-api](https://cmake.org/cmake/help/latest/manual/cmake-file-api.7.html) +- Writing queries +- Reading replies + +Dual-licensed under MIT or the [UNLICENSE](https://unlicense.org). + +### Usage + +Run `cargo add cmake-file-api` to add the crate to your project. + +### Example + +Build query and parse cmake-file-api: + +```rust +use cmake_file_api::{objects, query, reply}; + +fn main() -> Result<(), Box> { + let source_dir = std::path::Path::new("path/to/source/dir"); + let build_dir = std::path::Path::new("path/to/build/dir"); + + // write query for codemodel-v2 + query::Writer::default() + .request_object::() + .write_stateless(build_dir)?; + + // run cmake + assert!(std::process::Command::new("cmake") + .arg("-S") + .arg(source_dir) + .arg("-B") + .arg(build_dir) + .status()? + .success()); + + // parse cmake-file-api + let reader = reply::Reader::from_build_dir(build_dir)?; + + // read and print codemodel-v2 + let codemodel: objects::CodeModelV2 = reader.read_object()?; + codemodel.configurations.iter().for_each(|config| { + config.targets.iter().for_each(|target| { + println!("{}", target.name); + println!("{:#?}", target.sources); + }); + }); + + Ok(()) +} +``` + +# CMake-file-api +The `cmake-file-api` is the predecessor of the `cmaker-server` and was introduced in `CMake` 3.14. It provides a rich interface for querying configuration and project information. +The API is versioned, and the current version is v1. As the name suggests, the API is based on files, which are written to disk by `CMake` and read by client tools. `CMake` generates these files in a directory named `.cmake/api/v1` in the build directory. +The V1 API is a collection of JSON files that describe the configuration of the `CMake` project, and it always contains an `index-*.json` file which lists all available objects. +The objects are also versioned on their own, e.g. `codemodel-v2.json`. `CMake` will generate the files on demand, +and expects clients to first write queries inside `.cmake/api/v1/query` before configuration. +The query describes which objects the client is interested in. With stateful queries, the client can also provide additional client data which is available in the reply. +The API is commonly used insides IDE's but can also be used for other tooling purposes like invoking tools which need compile flags. + +# Related projects +- [python-cmake-file-api](https://github.com/madebr/python-cmake-file-api): Python bindings for the CMake File API +- [cfi-java](https://github.com/WalkerKnapp/cfi-java): Java bindings for the CMake File API diff --git a/UNLICENSE b/UNLICENSE new file mode 100644 index 0000000..00d2e13 --- /dev/null +++ b/UNLICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to \ No newline at end of file diff --git a/examples/basic.rs b/examples/basic.rs new file mode 100644 index 0000000..8e7baa5 --- /dev/null +++ b/examples/basic.rs @@ -0,0 +1,95 @@ +use cmake_file_api::{objects, reply}; +use std::path::PathBuf; +use std::process::ExitCode; + +/// This example demonstrates how to use `cmake_file_api` to get information about a `CMake` project. +fn main() -> Result> { + // source directory from argument or default to examples/cpp + let source_dir = if let Some(arg) = std::env::args().nth(1) { + PathBuf::from(arg) + } else { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("examples") + .join("cpp") + }; + + // build directory from argument or default to examples/cpp/build + let build_dir = if let Some(arg) = std::env::args().nth(2) { + PathBuf::from(arg) + } else { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("examples") + .join("cpp") + .join("build") + }; + + if !source_dir.exists() { + println!("Source directory does not exist: {}", source_dir.display()); + return Ok(ExitCode::FAILURE); + } + + if !build_dir.exists() { + println!("Creating build directory: {}", build_dir.display()); + std::fs::create_dir_all(&build_dir).unwrap(); + } + + if !reply::is_available(&build_dir) { + println!("CMake File API is not available, generating it now"); + + println!("Writing CMake File API query"); + cmake_file_api::query::Writer::default() + .request_object::() + .write_stateless(&build_dir)?; + + // run cmake + println!("Running cmake"); + assert!(std::process::Command::new("cmake") + .arg("-S") + .arg(&source_dir) + .arg("-B") + .arg(&build_dir) + .status()? + .success()); + } + + // load the file api + println!("Loading CMake File API"); + let reader = reply::Reader::from_build_dir(&build_dir)?; + + // get the codemodel + let codemodel: objects::CodeModelV2 = reader.read_object()?; + + // print all source files and their compile flags + for target in &codemodel.configurations[0].targets { + if target.sources.is_empty() { + continue; + } + + println!("Source files for target {}:", target.name); + for source in &target.sources { + println!(" {}", source.path.display()); + + if let Some(compile_group) = &source + .compile_group_index + .and_then(|i| target.compile_groups.get(i)) + { + println!(" Includes:"); + for include in &compile_group.includes { + println!(" * {}", include.path.display()); + } + + println!(" Defines:"); + for define in compile_group.defines() { + println!(" * {define}"); + } + + println!(" Flags:"); + for flag in compile_group.flags() { + println!(" * {flag}"); + } + } + } + } + + Ok(ExitCode::SUCCESS) +} diff --git a/examples/cpp/CMakeLists.txt b/examples/cpp/CMakeLists.txt new file mode 100644 index 0000000..470d161 --- /dev/null +++ b/examples/cpp/CMakeLists.txt @@ -0,0 +1,5 @@ + +cmake_minimum_required(VERSION 3.21) +project(cmake-file-api-example LANGUAGES CXX) + +add_executable(cmake-file-api-example main.cpp) \ No newline at end of file diff --git a/examples/cpp/main.cpp b/examples/cpp/main.cpp new file mode 100644 index 0000000..5fd7a6f --- /dev/null +++ b/examples/cpp/main.cpp @@ -0,0 +1,5 @@ + + +int main(int argc, char **argv) { + return 0; +} \ No newline at end of file diff --git a/examples/example_from_readme.rs b/examples/example_from_readme.rs new file mode 100644 index 0000000..242d03c --- /dev/null +++ b/examples/example_from_readme.rs @@ -0,0 +1,34 @@ +use cmake_file_api::{objects, query, reply}; + +fn main() -> Result<(), Box> { + let source_dir = std::path::Path::new("."); + let build_dir = std::path::Path::new("."); + + // write query for codemodel-v2 + query::Writer::default() + .request_object::() + .write_stateless(build_dir)?; + + // run cmake + assert!(std::process::Command::new("cmake") + .arg("-S") + .arg(source_dir) + .arg("-B") + .arg(build_dir) + .status()? + .success()); + + // parse cmake-file-api + let reader = reply::Reader::from_build_dir(build_dir)?; + + // read and print codemodel-v2 + let codemodel: objects::CodeModelV2 = reader.read_object()?; + codemodel.configurations.iter().for_each(|config| { + config.targets.iter().for_each(|target| { + println!("{}", target.name); + println!("{:#?}", target.sources); + }); + }); + + Ok(()) +} diff --git a/src/index.rs b/src/index.rs new file mode 100644 index 0000000..cb35b21 --- /dev/null +++ b/src/index.rs @@ -0,0 +1,502 @@ +use crate::objects::{MajorMinor, ObjectKind}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; +use std::path::PathBuf; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +#[non_exhaustive] +pub struct Index { + /// information about the instance of `CMake` that generated the reply + pub cmake: CMake, + + /// list of objects that are referenced in the reply + pub objects: Vec, + + /// map of replies to client queries + pub reply: HashMap, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +#[non_exhaustive] +pub struct CMake { + pub version: CMakeVersion, + pub paths: CMakePaths, + pub generator: CMakeGenerator, +} + +/// information about the instance of `CMake` that generated the reply +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +#[non_exhaustive] +pub struct CMakeVersion { + /// specifying the major version component + pub major: i32, + + /// specifying the minor version component + pub minor: i32, + + /// specifying the patch version component + pub patch: i32, + + /// specifying the version suffix, if any, e.g. g0abc3 + pub suffix: String, + + /// specifying the full version in the format `..[-]` + pub string: String, + + /// indicating whether the version was built from a version controlled source tree with local modifications + pub is_dirty: bool, +} + +/// paths to things that come with `CMake` +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +#[non_exhaustive] +pub struct CMakePaths { + /// absolute path to cmake tool + pub cmake: PathBuf, + + /// absolute path to ctest tool + pub ctest: PathBuf, + + /// absolute path to cpack tool + pub cpack: PathBuf, + + /// absolute path to the directory containing CMake resources like the Modules/ directory + pub root: PathBuf, +} + +/// describing the `CMake` generator used for the build +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +#[non_exhaustive] +pub struct CMakeGenerator { + /// specifying whether the generator supports multiple output configurations + pub multi_config: bool, + + /// specifying the name of the generator + pub name: String, + + /// If the generator supports CMAKE_GENERATOR_PLATFORM, this is a string specifying the generator platform name + pub platform: Option, +} + +/// represents a reference to another reply file +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +#[non_exhaustive] +pub struct ReplyFileReference { + /// specifying one of the Object Kinds + pub kind: ObjectKind, + + /// object version + pub version: MajorMinor, + + /// path relative to the reply index file to another JSON file containing the object + pub json_file: PathBuf, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +#[non_exhaustive] +pub struct Error { + pub error: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +#[non_exhaustive] +pub enum ClientField { + Error(Error), + ReplyFileReference(ReplyFileReference), + QueryJson(QueryJson), +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +#[non_exhaustive] +pub enum ReplyField { + Error(Error), + ReplyFileReference(ReplyFileReference), + Client(HashMap), + #[default] + Unknown, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +#[non_exhaustive] +pub struct QueryJson { + pub client: Option, + pub requests: Option, + pub responses: Option, +} + +#[cfg(test)] +mod testing { + use super::*; + use serde_json::json; + use std::collections::HashMap; + + #[test] + fn test_cmake() { + let json = json!({ + "generator" : + { + "multiConfig" : true, + "name" : "Visual Studio 16 2019", + "platform" : "x64" + }, + "paths" : + { + "cmake" : "C:/Program Files/CMake/bin/cmake.exe", + "cpack" : "C:/Program Files/CMake/bin/cpack.exe", + "ctest" : "C:/Program Files/CMake/bin/ctest.exe", + "root" : "C:/Program Files/CMake/share/cmake-3.27" + }, + "version" : { + "isDirty": false, + "major": 3, + "minor": 27, + "patch": 7, + "string": "3.27.7", + "suffix": "" + } + }); + + let cmake = serde_json::from_value::(json).unwrap(); + + assert_eq!( + cmake, + CMake { + version: CMakeVersion { + is_dirty: false, + major: 3, + minor: 27, + patch: 7, + string: "3.27.7".into(), + suffix: String::new(), + }, + paths: CMakePaths { + cmake: "C:/Program Files/CMake/bin/cmake.exe".into(), + cpack: "C:/Program Files/CMake/bin/cpack.exe".into(), + ctest: "C:/Program Files/CMake/bin/ctest.exe".into(), + root: "C:/Program Files/CMake/share/cmake-3.27".into(), + }, + generator: CMakeGenerator { + multi_config: true, + platform: Some("x64".into()), + name: "Visual Studio 16 2019".into(), + }, + } + ); + } + + #[test] + fn test_cmake_with_unknown_field() { + let json = json!({ + "generator" : + { + "multiConfig" : true, + "name" : "Visual Studio 16 2019", + "platform" : "x64", + "test" : "test" + }, + "paths" : + { + "cmake" : "C:/Program Files/CMake/bin/cmake.exe", + "cpack" : "C:/Program Files/CMake/bin/cpack.exe", + "ctest" : "C:/Program Files/CMake/bin/ctest.exe", + "root" : "C:/Program Files/CMake/share/cmake-3.27" + }, + "version" : { + "isDirty": false, + "major": 3, + "minor": 27, + "patch": 7, + "string": "3.27.7", + "suffix": "" + } + }); + + assert_eq!( + serde_json::from_value::(json) + .unwrap_err() + .to_string(), + "unknown field `test`, expected one of `multiConfig`, `name`, `platform`" + ); + } + + #[test] + fn test_objects() { + let json = json!([ + { + "jsonFile" : "codemodel-v2-b29a741ae0dbe513e631.json", + "kind" : "codemodel", + "version" : + { + "major" : 2, + "minor" : 6 + } + }, + { + "jsonFile" : "configureLog-v1-cac906d276896c7cc320.json", + "kind" : "configureLog", + "version" : + { + "major" : 1, + "minor" : 0 + } + } + ]); + + let objects = serde_json::from_value::>(json).unwrap(); + assert_eq!( + objects, + vec![ + ReplyFileReference { + json_file: "codemodel-v2-b29a741ae0dbe513e631.json".into(), + kind: ObjectKind::CodeModel, + version: MajorMinor { major: 2, minor: 6 } + }, + ReplyFileReference { + json_file: "configureLog-v1-cac906d276896c7cc320.json".into(), + kind: ObjectKind::ConfigureLog, + version: MajorMinor { major: 1, minor: 0 } + } + ] + ); + } + + #[test] + fn test_reply_with_error() { + let json = json!({ + "test_error" : + { + "error" : "test error" + } + }); + + let reply = serde_json::from_value::>(json).unwrap(); + let item = reply.iter().next().unwrap(); + + assert!(match item.1 { + ReplyField::Error(e) => e.error == "test error", + _ => false, + }); + } + #[test] + fn test_reply_with_reply_ref() { + let json = json!({ + "codemodel-v2" : + { + "jsonFile" : "codemodel-v2-b29a741ae0dbe513e631.json", + "kind" : "codemodel", + "version" : + { + "major" : 2, + "minor" : 6 + } + } + }); + + let reply = serde_json::from_value::>(json).unwrap(); + let item = reply.iter().next().unwrap(); + assert_eq!(item.0, "codemodel-v2"); + assert!(match item.1 { + ReplyField::ReplyFileReference(e) => + *e == ReplyFileReference { + json_file: "codemodel-v2-b29a741ae0dbe513e631.json".into(), + kind: ObjectKind::CodeModel, + version: MajorMinor { major: 2, minor: 6 }, + }, + _ => false, + }); + } + + #[test] + fn test_reply_client_with_reply_ref() { + let json = json!({ + "codemodel-v2" : + { + "jsonFile" : "codemodel-v2-b29a741ae0dbe513e631.json", + "kind" : "codemodel", + "version" : + { + "major" : 2, + "minor" : 6 + } + } + }); + + let reply = serde_json::from_value::>(json).unwrap(); + let item = reply.iter().next().unwrap(); + assert_eq!(item.0, "codemodel-v2"); + assert!(match item.1 { + ClientField::ReplyFileReference(e) => + *e == ReplyFileReference { + json_file: "codemodel-v2-b29a741ae0dbe513e631.json".into(), + kind: ObjectKind::CodeModel, + version: MajorMinor { major: 2, minor: 6 }, + }, + _ => false, + }); + } + + #[test] + fn test_reply_client_with_error() { + let json = json!({ + "bad_query.json" : + { + "error" : "unknown query file" + } + }); + + let reply = serde_json::from_value::>(json).unwrap(); + let item = reply.iter().next().unwrap(); + assert_eq!(item.0, "bad_query.json"); + assert!(match item.1 { + ClientField::Error(e) => e.error == "unknown query file", + _ => false, + }); + } + + #[test] + fn test_reply_query_json_with_client() { + let json = json!({ + "client" : + { + "myData" : 10 + }, + }); + + let query_json = serde_json::from_value::(json).unwrap(); + assert_eq!(query_json.client.unwrap()["myData"], 10); + } + + #[test] + fn test_reply_query_json_with_requests() { + let json = json!({ + "requests" : + [ + { + "kind" : "codemodel", + "version" : 2 + } + ] + }); + + let query_json = serde_json::from_value::(json).unwrap(); + assert!(query_json + .requests + .unwrap() + .as_array() + .unwrap() + .first() + .unwrap() + .is_object()); + } + + #[test] + fn test_reply_query_json_with_responses() { + let json = json!({ + "responses" : + [ + { + "jsonFile" : "codemodel-v2-b29a741ae0dbe513e631.json", + "kind" : "codemodel", + "version" : + { + "major" : 2, + "minor" : 6 + } + }, + { + "error": "error" + } + ] + }); + + let query_json = serde_json::from_value::(json).unwrap(); + assert!(query_json + .responses + .unwrap() + .as_array() + .unwrap() + .first() + .unwrap() + .is_object()); + } + #[test] + fn test_reply_query_json_with_response_error() { + let json = json!({ + "responses" : + { + "error" : "unknown request kind 'bad_name'" + } + }); + + let query_json = serde_json::from_value::(json).unwrap(); + assert!(query_json.responses.unwrap().is_object()); + } + + #[test] + fn test_index() { + let json = json!({ + "cmake": { + "version": { + "major": 3, "minor": 14, "patch": 0, "suffix": "", + "string": "3.14.0", "isDirty": false + }, + "paths": { + "cmake": "/prefix/bin/cmake", + "ctest": "/prefix/bin/ctest", + "cpack": "/prefix/bin/cpack", + "root": "/prefix/share/cmake-3.14" + }, + "generator": { + "multiConfig": false, + "name": "Unix Makefiles" + } + }, + "objects": [ + { "kind": "codemodel", + "version": { "major": 1, "minor": 0 }, + "jsonFile": "test.json" }, + ], + "reply": { + "-v": { "kind": "codemodel", + "version": { "major": 1, "minor": 0 }, + "jsonFile": "test.json" }, + "": { "error": "unknown query file" }, + "client-": { + "-v": { "kind": "codemodel", + "version": { "major": 1, "minor": 0 }, + "jsonFile": "test.json" }, + "": { "error": "unknown query file" }, + "query.json": { + "requests": [ {}, {}, {} ], + "responses": [ + { "kind": "codemodel", + "version": { "major": 1, "minor": 0 }, + "jsonFile": "test.json" }, + { "error": "unknown query file" }, + ], + "client": {} + } + } + } + }); + + serde_json::from_value::(json).unwrap(); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..46077dc --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,69 @@ +//! Library for interacting with the [cmake-file-api](https://cmake.org/cmake/help/latest/manual/cmake-file-api.7.html) +//! - Writing queries +//! - Reading replies +//! +//! # Example +//! +//! Build query and parse cmake-file-api +//! +//! ```no_run +//! # use std::error::Error; +//! # use std::path::Path; +//! use cmake_file_api::{query, reply, objects}; +//! +//! # +//! # fn try_main() -> Result<(), Box> { +//! # let source_dir = Path::new("."); +//! # let build_dir = Path::new("."); +//! +//! // generate query +//! query::Writer::default() +//! .request_object::() +//! .write_stateless(&build_dir)?; +//! +//! // run cmake +//! assert!(std::process::Command::new("cmake") +//! .arg("-S") +//! .arg(&source_dir) +//! .arg("-B") +//! .arg(&build_dir) +//! .status()? +//! .success()); +//! +//! // parse cmake-file-api +//! let reader = reply::Reader::from_build_dir(build_dir)?; +//! +//! // interact with api objects +//! let codemodel: objects::CodeModelV2 = reader.read_object()?; +//! for config in &codemodel.configurations{ +//! for target in &config.targets { +//! println!("{}", target.name); +//! println!("{:#?}", target.sources) +//! } +//! } +//! # Ok(()) +//! # } +//! ``` +//! +//! # cmake-file-api +//! The `CMake File API` is a new feature in `CMake` 3.14 that provides a rich interface for querying `CMake's` configuration and project information. +//! As the name suggests, the API is based on files, which are written to disk by `CMake` and read by client tools. +//! `CMake` generates these files in a directory named `.cmake/api/v1` in the build directory. The API is versioned, and the current version is v1. +//! The V1 API is a collection of JSON files that describe the configuration of the `CMake` project it always contains an `index-*.json` file which lists all available objects. +//! The objects are also versioned on their own, e.g. `codemodel-v2.json`. `CMake` will generate the files on demand, +//! and expects clients to first write a query file to the query directory `.cmake/api/v1/query` before configuration step. +//! The query describes which objects the client is interested in. With stateful queries, the client can also provide additional client data which is available in the reply. +//! The API is designed to be used by tools that need to interact with `CMake` (IDE) but can also be used for other tooling purposes e.g. generate `compile_commands.json`. +//! +//! +#![allow(clippy::implicit_return)] +#![allow(clippy::missing_inline_in_public_items)] +#![allow(clippy::question_mark_used)] +#![allow(clippy::missing_docs_in_private_items)] +#![allow(clippy::missing_trait_methods)] +#[allow(clippy::self_named_module_files)] + +pub mod index; +pub mod objects; +pub mod query; +pub mod reply; diff --git a/src/objects.rs b/src/objects.rs new file mode 100644 index 0000000..944c1c8 --- /dev/null +++ b/src/objects.rs @@ -0,0 +1,71 @@ +use serde::{Deserialize, Serialize}; + +pub mod cache_v2; +pub mod cmake_files_v1; +pub mod codemodel_v2; +pub mod configure_log_v1; +pub mod toolchains_v1; + +pub use cache_v2::Cache as CacheV2; +pub use cmake_files_v1::CMakeFiles as CMakeFilesV1; +pub use codemodel_v2::CodeModel as CodeModelV2; +pub use configure_log_v1::ConfigureLog as ConfigureLogV1; +pub use toolchains_v1::Toolchains as ToolchainsV1; + +use crate::reply; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct MajorMinor { + pub major: u32, + pub minor: u32, +} + +#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] +#[non_exhaustive] +pub enum ObjectKind { + #[default] + #[serde(rename = "codemodel")] + CodeModel, + #[serde(rename = "toolchains")] + Toolchains, + #[serde(rename = "cache")] + Cache, + #[serde(rename = "cmakeFiles")] + CMakeFiles, + #[serde(rename = "configureLog")] + ConfigureLog, +} + +impl ObjectKind { + #[must_use] + pub fn as_str(&self) -> &'static str { + match self { + ObjectKind::CodeModel => "codemodel", + ObjectKind::Toolchains => "toolchains", + ObjectKind::Cache => "cache", + ObjectKind::CMakeFiles => "cmakeFiles", + ObjectKind::ConfigureLog => "configureLog", + } + } +} + +pub trait Object { + fn kind() -> ObjectKind; + fn major() -> u32; + + /// Resolve references in the object + /// + /// Some objects contain references to other json files. This method is called after the object + /// is deserialized to resolve these references. + /// Currently only the codemodel-v2 object has references (targets, directories) that need to be resolved. + /// + /// # Errors + /// + /// `ReaderError::IO`: if an IO error occurs while reading the object file + /// `ReaderError::Parse`: if an error occurs while parsing the object file + fn resolve_references(&mut self, _: &reply::Reader) -> Result<(), reply::ReaderError> { + Ok(()) + } +} diff --git a/src/objects/cache_v2.rs b/src/objects/cache_v2.rs new file mode 100644 index 0000000..523e3e1 --- /dev/null +++ b/src/objects/cache_v2.rs @@ -0,0 +1,125 @@ +use crate::objects::{MajorMinor, Object, ObjectKind}; +use serde::{Deserialize, Serialize}; + +/// The cache object kind lists cache entries. +/// These are the Variables stored in the persistent cache (CMakeCache.txt) for the build tree. +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Cache { + /// Kind of the cache object + pub kind: ObjectKind, + + /// Version of the cache object + pub version: MajorMinor, + + /// Entries in the cache + pub entries: Vec, +} + +/// Entry in the cache +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Entry { + /// Name of the entry + pub name: String, + + /// Value of the entry + pub value: String, + + /// Type of the entry + #[serde(rename = "type")] + pub type_name: String, + + /// Properties of the entry + pub properties: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Property { + /// Name of the property + pub name: String, + + /// Value of the property + pub value: String, +} + +impl Object for Cache { + fn kind() -> ObjectKind { + ObjectKind::Cache + } + + fn major() -> u32 { + 2 + } +} + +#[cfg(test)] +mod tests { + use crate::objects::cache_v2::*; + use serde_json::json; + + #[test] + fn test_configure_log() { + let json = json!({ + "kind": "cache", + "version": { "major": 2, "minor": 0 }, + "entries": [ + { + "name": "BUILD_SHARED_LIBS", + "value": "ON", + "type": "BOOL", + "properties": [ + { + "name": "HELPSTRING", + "value": "Build shared libraries" + } + ] + }, + { + "name": "CMAKE_GENERATOR", + "value": "Unix Makefiles", + "type": "INTERNAL", + "properties": [ + { + "name": "HELPSTRING", + "value": "Name of generator." + } + ] + } + ] + }); + + let cache = serde_json::from_value::(json).unwrap(); + assert_eq!( + cache, + Cache { + kind: ObjectKind::Cache, + version: MajorMinor { major: 2, minor: 0 }, + entries: vec![ + Entry { + name: "BUILD_SHARED_LIBS".into(), + value: "ON".into(), + type_name: "BOOL".into(), + properties: vec![Property { + name: "HELPSTRING".into(), + value: "Build shared libraries".into(), + }] + }, + Entry { + name: "CMAKE_GENERATOR".into(), + value: "Unix Makefiles".into(), + type_name: "INTERNAL".into(), + properties: vec![Property { + name: "HELPSTRING".into(), + value: "Name of generator.".into(), + }] + } + ] + } + ); + } +} diff --git a/src/objects/cmake_files_v1.rs b/src/objects/cmake_files_v1.rs new file mode 100644 index 0000000..6f3ee63 --- /dev/null +++ b/src/objects/cmake_files_v1.rs @@ -0,0 +1,136 @@ +use crate::objects::{MajorMinor, Object, ObjectKind}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +/// The cmakeFiles object kind lists files used by `CMake` while configuring and generating the build system. +/// These include the CMakeLists.txt files as well as included .cmake files. +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct CMakeFiles { + /// Kind of the CMakeFiles object. + pub kind: ObjectKind, + + /// Version of the CMakeFiles object. + pub version: MajorMinor, + + /// Paths of the CMakeFiles object. + pub paths: Paths, + + /// Input file used by CMake when configuring and generating the build system. + pub inputs: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Paths { + /// Absolute path to the top-level source directory, represented with forward slashes. + pub build: PathBuf, + + /// Absolute path to the top-level build directory, represented with forward slashes. + pub source: PathBuf, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Input { + /// path to an input file to CMake, represented with forward slashes. + /// If the file is inside the top-level source directory then the path is specified relative to that directory. + /// Otherwise, the path is absolute. + pub path: PathBuf, + + /// True if the path specifies a file that is under the top-level build directory and the build is out-of-source. + #[serde(default)] + pub is_generated: bool, + + /// True if the path specifies a file that is not under the top-level source or build directories. + #[serde(default)] + pub is_external: bool, + + /// True if the path specifies a file in the CMake installation. + #[serde(default, rename = "isCMake")] + pub is_cmake: bool, +} + +impl Object for CMakeFiles { + fn kind() -> ObjectKind { + ObjectKind::CMakeFiles + } + + fn major() -> u32 { + 1 + } +} + +#[cfg(test)] +mod tests { + use crate::objects::cmake_files_v1::*; + use serde_json::json; + + #[test] + fn test_configure_log() { + let json = json!({ + "kind": "cmakeFiles", + "version": { "major": 1, "minor": 0 }, + "paths": { + "build": "/path/to/top-level-build-dir", + "source": "/path/to/top-level-source-dir" + }, + "inputs": [ + { + "path": "CMakeLists.txt" + }, + { + "isGenerated": true, + "path": "/path/to/top-level-build-dir/../CMakeSystem.cmake" + }, + { + "isExternal": true, + "path": "/path/to/external/third-party/module.cmake" + }, + { + "isCMake": true, + "isExternal": true, + "path": "/path/to/cmake/Modules/CMakeGenericSystem.cmake" + } + ] + }); + + let cmake_files = serde_json::from_value::(json).unwrap(); + assert_eq!( + cmake_files, + CMakeFiles { + kind: ObjectKind::CMakeFiles, + version: MajorMinor { major: 1, minor: 0 }, + paths: Paths { + build: "/path/to/top-level-build-dir".into(), + source: "/path/to/top-level-source-dir".into() + }, + inputs: vec![ + Input { + path: "CMakeLists.txt".into(), + ..Default::default() + }, + Input { + is_generated: true, + path: "/path/to/top-level-build-dir/../CMakeSystem.cmake".into(), + ..Default::default() + }, + Input { + is_external: true, + path: "/path/to/external/third-party/module.cmake".into(), + ..Default::default() + }, + Input { + is_cmake: true, + is_external: true, + path: "/path/to/cmake/Modules/CMakeGenericSystem.cmake".into(), + ..Default::default() + } + ] + } + ); + } +} diff --git a/src/objects/codemodel_v2.rs b/src/objects/codemodel_v2.rs new file mode 100644 index 0000000..cd29c7b --- /dev/null +++ b/src/objects/codemodel_v2.rs @@ -0,0 +1,9 @@ +pub mod backtrace_graph; +pub mod codemodel; +pub mod directory; +pub mod target; + +pub use backtrace_graph::*; +pub use codemodel::*; +pub use directory::*; +pub use target::*; diff --git a/src/objects/codemodel_v2/backtrace_graph.rs b/src/objects/codemodel_v2/backtrace_graph.rs new file mode 100644 index 0000000..ce9535b --- /dev/null +++ b/src/objects/codemodel_v2/backtrace_graph.rs @@ -0,0 +1,112 @@ +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +/// The backtraceGraph member of a "codemodel" version 2 "directory" object, or "codemodel" version 2 "target" object. +/// Describes a graph of backtraces. +/// Its nodes are referenced from backtrace members elsewhere in the containing object. +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct BacktraceGraph { + /// Backtrace nodes. + pub nodes: Vec, + + /// Command names referenced by backtrace nodes. + /// Each entry is a string specifying a command name. + pub commands: Vec, + + /// CMake's language files referenced by backtrace nodes + /// Each entry is a path to a file, represented with forward slashes. + /// If the file is inside the top-level source directory then the path is specified relative to that directory. + /// Otherwise, the path is absolute. + pub files: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Node { + /// An unsigned integer 0-based index into the backtrace files array. + pub file: usize, + + /// An optional member present when the node represents a line within the file. + /// The value is an unsigned integer 1-based line number. + pub line: Option, + + /// An optional member present when the node represents a command invocation within the file. + /// The value is an unsigned integer 0-based index into the backtrace commands array. + pub command: Option, + + /// An optional member present when the node is not the bottom of the call stack. + /// The value is an unsigned integer 0-based index of another entry in the backtrace nodes array. + pub parent: Option, +} + +#[cfg(test)] +mod tests { + use crate::objects::codemodel_v2::backtrace_graph::*; + use serde_json::json; + + #[test] + fn test_backtrace_graph() { + let json = json!({ + "commands" : + [ + "add_executable", + "target_link_libraries" + ], + "files" : + [ + "CMakeLists.txt" + ], + "nodes" : + [ + { + "file" : 0 + }, + { + "command" : 0, + "file" : 0, + "line" : 4, + "parent" : 0 + }, + { + "command" : 1, + "file" : 0, + "line" : 9, + "parent" : 0 + } + ] + }); + + let graph = serde_json::from_value::(json).unwrap(); + assert_eq!( + graph, + BacktraceGraph { + commands: vec![ + "add_executable".to_string(), + "target_link_libraries".to_string() + ], + files: vec![PathBuf::from("CMakeLists.txt")], + nodes: vec![ + Node { + file: 0, + ..Default::default() + }, + Node { + file: 0, + command: Some(0), + line: Some(4), + parent: Some(0) + }, + Node { + file: 0, + command: Some(1), + line: Some(9), + parent: Some(0) + } + ] + } + ); + } +} diff --git a/src/objects/codemodel_v2/codemodel.rs b/src/objects/codemodel_v2/codemodel.rs new file mode 100644 index 0000000..11c5dee --- /dev/null +++ b/src/objects/codemodel_v2/codemodel.rs @@ -0,0 +1,316 @@ +#![allow(clippy::module_name_repetitions)] + +use crate::objects::codemodel_v2::{Directory, Target}; +use crate::objects::{MajorMinor, Object, ObjectKind}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use crate::reply; + +/// The codemodel object kind describes the build system structure as modeled by `CMake`. +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct CodeModel { + /// Kind of the codemodel object. + pub kind: ObjectKind, + + /// Version of the codemodel object. + pub version: MajorMinor, + + /// Paths of the codemodel object. + pub paths: CodemodelPaths, + + /// Available build configurations. + /// On single-configuration generators there is one entry for the value of the CMAKE_BUILD_TYPE variable. + /// For multi-configuration generators there is an entry for each configuration listed in the CMAKE_CONFIGURATION_TYPES variable. + pub configurations: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct CodemodelPaths { + /// Absolute path to the top-level source directory, represented with forward slashes. + pub build: PathBuf, + + /// Absolute path to the top-level build directory, represented with forward slashes. + pub source: PathBuf, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Configuration { + /// A string specifying the name of the configuration, e.g. Debug. + pub name: String, + + /// Top-level project and subprojects defined in the build system. + /// Each (sub-)project corresponds to a source directory whose CMakeLists.txt file calls the project() command with a project name different from its parent directory. + /// The first entry corresponds to the top-level project. + pub projects: Vec, + + /// Build system directory info whose source directory contains a CMakeLists.txt file. + /// The first entry corresponds to the top-level directory + #[serde(rename = "directories")] + pub directory_refs: Vec, + + /// Build system targets. + /// Such targets are created by calls to add_executable(), add_library(), and add_custom_target(), + /// excluding imported targets and interface libraries (which do not generate any build rules). + #[serde(rename = "targets")] + pub target_refs: Vec, + + /// The following members are not part of the JSON file. + /// They are used to store the actual objects that the references point to. + + /// Directory objects. + /// The position in the vector corresponds to the index in the directory_refs vector. + #[serde(skip)] + pub directories: Vec, + + /// Target objects. + /// The position in the vector corresponds to the index in the target_refs vector. + #[serde(skip)] + pub targets: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct DirectoryReference { + /// Path to the source directory, represented with forward slashes. + /// If the directory is inside the top-level source directory then the path is specified + /// relative to that directory (with . for the top-level source directory itself). + /// Otherwise, the path is absolute. + pub source: PathBuf, + + /// Path to the build directory, represented with forward slashes. + /// If the directory is inside the top-level build directory then the path is specified + /// relative to that directory (with . for the top-level build directory itself). + /// Otherwise, the path is absolute. + pub build: PathBuf, + + /// Optional member that is present when the directory is not top-level. + /// The value is an unsigned integer 0-based index of another entry in the main directories array + /// that corresponds to the parent directory that added this directory as a subdirectory. + pub parent_index: Option, + + /// Optional member that is present when the directory has subdirectories. + /// Each entry corresponding to child directory created by the add_subdirectory() or subdirs() command. + /// Each entry is an unsigned integer 0-based index of another entry in the main directories array. + #[serde(default)] + pub child_indexes: Vec, + + /// An unsigned integer 0-based index into the main projects array indicating the build system project to which the directory belongs. + pub project_index: usize, + + /// Optional member that is present when the directory itself has targets, excluding those belonging to subdirectories. + /// Each entry corresponding to the targets. + /// Each entry is an unsigned integer 0-based index into the main targets array. + #[serde(default)] + pub target_indexes: Vec, + + /// Optional member present when a minimum required version of CMake is known for the directory. + /// This is the `` version given to the most local call to the cmake_minimum_required(VERSION) command in the directory itself or + /// one of its ancestors. + #[serde(rename = "minimumCMakeVersion")] + pub minimum_cmake_version: Option, + + /// True when the directory or one of its subdirectories contains any install() rules, i.e. whether a make install or equivalent rule is available. + #[serde(default)] + pub has_install_rule: bool, + + /// Path relative to the codemodel file to another JSON file containing a "codemodel" version 2 "directory" object. + pub json_file: PathBuf, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct MinimumCmakeVersion { + /// A string specifying the minimum required version in the format + /// \.\.\[\\[.\]]\[\] + /// Each component is an unsigned integer and the suffix may be an arbitrary string. + #[serde(rename = "string")] + pub version: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Project { + /// A string specifying the name given to the project() command. + pub name: String, + + /// Optional member that is present when the project is not top-level. + /// The value is an unsigned integer 0-based index of another entry in the main projects array that corresponds to the parent project + /// that added this project as a subproject. + pub parent_index: Option, + + /// Optional member that is present when the project has subprojects. + /// Entries corresponding to the subprojects. + /// Each entry is an unsigned integer 0-based index of another entry in the main projects array. + #[serde(default)] + pub child_indexes: Vec, + + /// Entries corresponding to build system directories that are part of the project. + /// The first entry corresponds to the top-level directory of the project. + /// Each entry is an unsigned integer 0-based index into the main directories array. + pub directory_indexes: Vec, + + /// Optional member that is present when the project itself has targets, excluding those belonging to subprojects. + /// Entries corresponding to the targets. + /// Each entry is an unsigned integer 0-based index into the main targets array. + #[serde(default)] + pub target_indexes: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct TargetReference { + /// A string specifying the target name. + pub name: String, + + /// A string uniquely identifying the target. + /// This matches the id field in the file referenced by jsonFile. + pub id: String, + + /// An unsigned integer 0-based index into the main directories array indicating + /// the build system directory in which the target is defined. + pub directory_index: usize, + + /// An unsigned integer 0-based index into the main projects array indicating the + /// build system project in which the target is defined. + pub project_index: usize, + + /// Path relative to the codemodel file to another JSON file containing a "codemodel" version 2 "target" object. + pub json_file: PathBuf, +} + +impl Object for CodeModel { + fn kind() -> ObjectKind { + ObjectKind::CodeModel + } + + fn major() -> u32 { + 2 + } + + fn resolve_references(&mut self, reader: &reply::Reader) -> Result<(), reply::ReaderError> { + let reply_dir = reply::dir(reader.build_dir()); + + // resolve targets and directories references + for config in &mut self.configurations { + for target_ref in &config.target_refs { + config + .targets + .push(reply::Reader::parse_reply(reply_dir.join(&target_ref.json_file))?); + } + + for directory_ref in &config.directory_refs { + config.directories.push(reply::Reader::parse_reply( + reply_dir.join(&directory_ref.json_file), + )?); + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::objects; + use crate::objects::codemodel_v2::*; + use crate::objects::MajorMinor; + use serde_json::json; + use std::path::PathBuf; + + #[test] + fn test_model() { + let json = json!({ + "kind": "codemodel", + "version": { "major": 2, "minor": 6 }, + "paths": { + "source": "/path/to/top-level-source-dir", + "build": "/path/to/top-level-build-dir" + }, + "configurations": [ + { + "name": "Debug", + "directories": [ + { + "source": ".", + "build": ".", + "childIndexes": [ 1 ], + "projectIndex": 0, + "targetIndexes": [ 0 ], + "hasInstallRule": true, + "minimumCMakeVersion": { + "string": "3.14" + }, + "jsonFile": "" + }, + { + "source": "sub", + "build": "sub", + "parentIndex": 0, + "projectIndex": 0, + "targetIndexes": [ 1 ], + "minimumCMakeVersion": { + "string": "3.14" + }, + "jsonFile": "" + } + ], + "projects": [ + { + "name": "MyProject", + "directoryIndexes": [ 0, 1 ], + "targetIndexes": [ 0, 1 ] + } + ], + "targets": [ + { + "name": "MyExecutable", + "directoryIndex": 0, + "projectIndex": 0, + "jsonFile": "", + "id": "0" + }, + { + "name": "MyLibrary", + "directoryIndex": 1, + "projectIndex": 0, + "jsonFile": "", + "id": "1" + } + ] + } + ] + }); + + let model = serde_json::from_value::(json).unwrap(); + assert_eq!(model.kind, objects::ObjectKind::CodeModel); + assert_eq!(model.version, MajorMinor { major: 2, minor: 6 }); + assert_eq!( + model.paths, + CodemodelPaths { + source: "/path/to/top-level-source-dir".into(), + build: "/path/to/top-level-build-dir".into() + } + ); + assert_eq!(model.configurations.len(), 1); + assert_eq!(model.configurations[0].name, "Debug"); + assert_eq!(model.configurations[0].directory_refs.len(), 2); + assert_eq!( + model.configurations[0].directory_refs[0].source, + PathBuf::from(".") + ); + assert_eq!(model.configurations[0].projects.len(), 1); + assert_eq!(model.configurations[0].projects[0].name, "MyProject"); + assert_eq!(model.configurations[0].target_refs.len(), 2); + assert_eq!(model.configurations[0].target_refs[0].name, "MyExecutable"); + } +} diff --git a/src/objects/codemodel_v2/directory.rs b/src/objects/codemodel_v2/directory.rs new file mode 100644 index 0000000..477da23 --- /dev/null +++ b/src/objects/codemodel_v2/directory.rs @@ -0,0 +1,224 @@ +#![allow(clippy::struct_excessive_bools)] +#![allow(clippy::module_name_repetitions)] + +use super::backtrace_graph::BacktraceGraph; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +/// A codemodel "directory" object is referenced by a "codemodel" version 2 object's directories array. +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Directory { + /// Paths of the directory object. + pub paths: DirectoryPaths, + + /// A "codemodel" version 2 "backtrace graph" whose nodes are referenced from backtrace members elsewhere in this "directory" object. + pub backtrace_graph: BacktraceGraph, + + /// Entries corresponding to install() rules + pub installers: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct DirectoryPaths { + /// A string specifying the path to the source directory, represented with forward slashes. + /// If the directory is inside the top-level source directory then the path is specified + /// relative to that directory (with . for the top-level source directory itself). + /// Otherwise, the path is absolute. + pub build: PathBuf, + + /// A string specifying the path to the build directory, represented with forward slashes. + /// If the directory is inside the top-level build directory then the path is specified + /// relative to that directory (with . for the top-level build directory itself). + /// Otherwise, the path is absolute. + pub source: PathBuf, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Installer { + /// A string specifying the component selected by the corresponding to install() command invocation. + pub component: String, + + /// Optional member that is present for specific type values below. The value is a string specifying the installation destination path. + /// The path may be absolute or relative to the installation prefix. + pub destination: Option, + + /// Optional member that is present for specific installer_type values below. + #[serde(default)] + pub paths: Vec, + + /// A string specifying the type of installation rule. The value is one of the following, with some variants providing additional members: + /// * file: An install(FILES) or install(PROGRAMS) call. The destination and paths members are populated, with paths under the top-level source directory expressed relative to it. The isOptional member may exist. This type has no additional members. + /// * directory: An install(DIRECTORY) call. The destination and paths members are populated, with paths under the top-level source directory expressed relative to it. The isOptional member may exist. This type has no additional members. + /// * target: An install(TARGETS) call. The destination and paths members are populated, with paths under the top-level build directory expressed relative to it. The isOptional member may exist. This type has additional members targetId, targetIndex, targetIsImportLibrary, and targetInstallNamelink. + /// * export: An install(EXPORT) call. The destination and paths members are populated, with paths under the top-level build directory expressed relative to it. The paths entries refer to files generated automatically by CMake for installation, and their actual values are considered private implementation details. This type has additional members exportName and exportTargets. + /// * script: An install(SCRIPT) call. This type has additional member scriptFile. + /// * code: An install(CODE) call. This type has no additional members. + /// * importedRuntimeArtifacts: An install(IMPORTED_RUNTIME_ARTIFACTS) call. The destination member is populated. The isOptional member may exist. This type has no additional members. + /// * runtimeDependencySet: An install(RUNTIME_DEPENDENCY_SET) call or an install(TARGETS) call with RUNTIME_DEPENDENCIES. The destination member is populated. This type has additional members runtimeDependencySetName and runtimeDependencySetType. + /// * fileSet: An install(TARGETS) call with FILE_SET. The destination and paths members are populated. The isOptional member may exist. This type has additional members fileSetName, fileSetType, fileSetDirectories, and fileSetTarget. + /// This type was added in codemodel version 2.4. + #[serde(rename = "type")] + pub installer_type: String, + + /// True when install() is called with the EXCLUDE_FROM_ALL option. + #[serde(default)] + pub is_exclude_from_all: bool, + + /// True when install(SCRIPT|CODE) is called with the ALL_COMPONENTS option. + #[serde(default)] + pub is_for_all_components: bool, + + /// True when install() is called with the OPTIONAL option. + /// This is allowed when type is file, directory, or target. + #[serde(default)] + pub is_optional: bool, + + /// Optional member that is present when type is target. The value is a string uniquely identifying the target to be installed. + /// This matches the id member of the target in the main "codemodel" object's targets array. + pub target_id: Option, + + /// Optional member that is present when type is target. + /// The value is an unsigned integer 0-based index into the main "codemodel" object's targets array for the target to be installed. + pub target_index: Option, + + /// True when type is target and the installer is for a Windows DLL import library file or for an AIX linker import file. + #[serde(default)] + pub target_is_import_library: bool, + + /// Optional member that is present when type is target and the installer corresponds to a target that may use symbolic links + /// to implement the VERSION and SOVERSION target properties. + /// The value is a string indicating how the installer is supposed to handle the symlinks: + /// skip means the installer should skip the symlinks and install only the real file + /// only means the installer should install only the symlinks and not the real file. + /// In all cases the paths member lists what it actually installs. + pub target_install_namelink: Option, + + /// Optional member that is present when type is export. + /// The value is a string specifying the name of the export. + pub export_name: Option, + + /// Optional member that is present when type equals export. + #[serde(default)] + pub export_targets: Vec, + + /// Optional member that is present when type is runtimeDependencySet and the installer was created by an install(RUNTIME_DEPENDENCY_SET) call. + /// The value is a string specifying the name of the runtime dependency set that was installed. + pub runtime_dependency_set_name: Option, + + /// Optional member that is present when type is runtimeDependencySet. + /// The value is a string with one of the following values: + /// * library: Indicates that this installer installs dependencies that are not macOS frameworks. + /// * framework: Indicates that this installer installs dependencies that are macOS frameworks. + pub runtime_dependency_set_type: Option, + + /// Optional member that is present when type is fileSet. The value is a string with the name of the file set. + /// This field was added in codemodel version 2.4. + pub file_set_name: Option, + + /// Optional member that is present when type is fileSet. The value is a string with the type of the file set. + /// This field was added in codemodel version 2.4. + pub file_set_type: Option, + + /// Optional member that is present when type is fileSet. + /// The value is a list of strings with the file set's base directories (determined by genex-evaluation of HEADER_DIRS or `HEADER_DIRS_`). + /// This field was added in codemodel version 2.4. + #[serde(default)] + pub file_set_directories: Vec, + + /// Optional member that is present when type is fileSet. + /// This field was added in codemodel version 2.4. + pub file_set_target: Option, + + /// Optional member that is present when type is script. + /// The value is a string specifying the path to the script file on disk, represented with forward slashes. + /// If the file is inside the top-level source directory then the path is specified relative to that directory. + /// Otherwise, the path is absolute. + pub script_file: Option, + + /// Optional member that is present when a CMake language backtrace to the install() or other command invocation + /// that added this installer is available. + /// The value is an unsigned integer 0-based index into the backtraceGraph member's nodes array. + pub backtrace: Option, +} +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct TargetIdAndIndex { + /// A string uniquely identifying the target. + /// This matches the id member of the target in the main "codemodel" object's targets array. + pub id: String, + + /// An unsigned integer 0-based index into the main "codemodel" object's targets array for the target. + pub index: usize, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct FromToPaths { + /// Path from which a file or directory is to be installed. + pub from: PathBuf, + + /// Path to which the file or directory is to be installed under the destination. + pub to: PathBuf, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +#[non_exhaustive] +pub enum InstallPath { + /// A string specifying the path from which a file or directory is to be installed. + /// The portion of the path not preceded by a / also specifies the path (name) to which the file or directory is to be installed + /// under the destination. + PathCombination(String), + + /// A pair of paths specifying the path from which a file or directory is to be installed and + /// the path to which the file or directory is to be installed under the destination. + FromTo(FromToPaths), +} + +#[cfg(test)] +mod tests { + use crate::objects::codemodel_v2::directory::*; + use serde_json::json; + use std::path::PathBuf; + + #[test] + fn test_directory() { + let json = json!({ + "backtraceGraph" : + { + "commands" : [], + "files" : [], + "nodes" : [] + }, + "installers" : [], + "paths" : + { + "build" : ".", + "source" : "." + } + }); + + let dir = serde_json::from_value::(json).unwrap(); + assert_eq!( + dir, + Directory { + backtrace_graph: BacktraceGraph { + ..Default::default() + }, + installers: vec![], + paths: DirectoryPaths { + build: PathBuf::from("."), + source: PathBuf::from(".") + } + } + ); + } +} diff --git a/src/objects/codemodel_v2/target.rs b/src/objects/codemodel_v2/target.rs new file mode 100644 index 0000000..9e31d1f --- /dev/null +++ b/src/objects/codemodel_v2/target.rs @@ -0,0 +1,567 @@ +#![allow(clippy::module_name_repetitions)] +#![allow(clippy::redundant_closure_for_method_calls)] + +use super::backtrace_graph::BacktraceGraph; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +/// A codemodel "target" object is referenced by a "codemodel" version 2 object's targets array. +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Target { + /// A string specifying the logical name of the target. + pub name: String, + + /// A string uniquely identifying the target. + /// The format is unspecified and should not be interpreted by clients. + pub id: String, + + /// A string specifying the type of the target. + /// The value is one of: + /// * EXECUTABLE + /// * STATIC_LIBRARY + /// * SHARED_LIBRARY + /// * MODULE_LIBRARY + /// * OBJECT_LIBRARY + /// * INTERFACE_LIBRARY + /// * UTILITY + #[serde(rename = "type")] + pub type_name: String, + + /// Optional member that is present when a CMake language backtrace to the command in + /// the source code that created the target is available. + /// The value is an unsigned integer 0-based index into the backtraceGraph member's nodes array. + pub backtrace: Option, + + /// Optional member that is present when the FOLDER target property is set. + pub folder: Option, + + /// Paths to the target's build and source directories. + pub paths: TargetPaths, + + /// Optional member that is present for executable and library targets that are linked or archived into a single primary artifact. + /// The value is a string specifying the file name of that artifact on disk. + pub name_on_disk: Option, + + /// Optional member that is present for executable and library targets that + /// produce artifacts on disk meant for consumption by dependents. + /// The value is a JSON array of entries corresponding to the artifacts. + #[serde(default)] + pub artifacts: Vec, + + /// Optional member that is present with boolean value true if the target is provided by CMake's + /// build system generator rather than by a command in the source code. + #[serde(default)] + pub is_generator_provided: bool, + + /// Optional member that is present when the target has an install() rule. + pub install: Option, + + /// Optional member that is present on executable targets that have at least one launcher specified by the project. + #[serde(default)] + pub launchers: Vec, + + /// Optional member that is present for executables and shared library targets that link into a runtime binary. + pub link: Option, + + /// Optional member that is present for static library targets. + pub archive: Option, + + /// Optional member that is present when the target depends on other targets. + #[serde(default)] + pub dependencies: Vec, + + /// target's file sets + #[serde(default)] + pub file_sets: Vec, + + /// target's sources + #[serde(default)] + pub sources: Vec, + + /// Optional member that is present when sources are grouped together by the source_group() command or by default. + #[serde(default)] + pub source_groups: Vec, + + /// Optional member that is present when the target has sources that compile. + #[serde(default)] + pub compile_groups: Vec, + + /// A "codemodel" version 2 "backtrace graph" whose nodes are referenced from backtrace members elsewhere in this "target" object. + pub backtrace_graph: BacktraceGraph, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Folder { + /// A string specifying the name of the target folder. + pub name: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct TargetPaths { + /// Path to the target's source directory, represented with forward slashes. + /// If the directory is inside the top-level source directory then the path is specified + /// relative to that directory (with . for the top-level source directory itself). + /// Otherwise, the path is absolute. + pub build: PathBuf, + + /// Path to the target's build directory, represented with forward slashes. + /// If the directory is inside the top-level build directory then the path is specified + /// relative to that directory (with . for the top-level build directory itself). + /// Otherwise, the path is absolute. + pub source: PathBuf, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Artifact { + /// Path to the file on disk, represented with forward slashes. + /// If the file is inside the top-level build directory then the path is specified + /// relative to that directory. + /// Otherwise, the path is absolute. + pub path: PathBuf, +} +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Install { + /// installation prefix + pub prefix: Prefix, + + /// installation destination paths + #[serde(default)] + pub destinations: Vec, +} +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Prefix { + /// Path value of CMAKE_INSTALL_PREFIX. + pub path: PathBuf, +} +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Destination { + /// Path of the installation destination path. + /// The path may be absolute or relative to the install prefix. + pub path: PathBuf, + + /// Optional member that is present when a CMake language backtrace to the install() command invocation + /// that specified this destination is available. + /// The value is an unsigned integer 0-based index into the backtraceGraph member's nodes array. + pub backtrace: Option, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Launcher { + /// string specifying the path to the launcher on disk, represented with forward slashes. + /// If the file is inside the top-level source directory then the path is specified relative to that directory. + pub command: String, + + /// Optional member that is present when the launcher command has arguments preceding the executable to be launched. + #[serde(default)] + pub arguments: Vec, + + /// A string specifying the type of launcher. + /// The value is one of the following: + /// * emulator: An emulator for the target platform when cross-compiling. See the CROSSCOMPILING_EMULATOR target property. + /// * test: A start program for the execution of tests. See the TEST_LAUNCHER target property. + pub launcher_type: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Link { + /// A string specifying the language (e.g. C, CXX, Fortran) of the toolchain is used to invoke the linker. + pub language: String, + + /// Optional member that is present when fragments of the link command line invocation are available. + #[serde(default)] + pub command_fragments: Vec, + + /// True when link-time optimization (a.k.a. interprocedural optimization or link-time code generation) is enabled. + #[serde(default)] + pub lto: bool, + + /// Optional member that is present when the CMAKE_SYSROOT_LINK or CMAKE_SYSROOT variable is defined. + #[serde(default)] + pub sysroot: Option, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct CommandFragment { + /// A string specifying a fragment of the link command line invocation. + /// The value is encoded in the build system's native shell format. + pub fragment: String, + + /// A string specifying the role of the fragment's content: + /// * flags: archiver flags + pub role: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct SysRootPath { + /// Absolute path to the sysroot, represented with forward slashes. + pub path: PathBuf, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Archive { + /// fragments of the archiver command line invocation. + #[serde(default)] + pub command_fragments: Vec, + + /// True when link-time optimization (a.k.a. interprocedural optimization or link-time code generation) is enabled. + #[serde(default)] + pub lto: bool, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Dependency { + /// A string uniquely identifying the target on which this target depends. + /// This matches the main id member of the other target. + pub id: String, + + /// Optional member that is present when a CMake language backtrace to the add_dependencies(), target_link_libraries(), + /// or other command invocation that created this dependency is available. + /// The value is an unsigned integer 0-based index into the backtraceGraph member's nodes array. + pub backtrace: Option, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct FileSet { + /// A string specifying the name of the file set. + pub name: String, + + /// A string specifying the type of the file set. See target_sources() supported file set types. + #[serde(rename = "type")] + pub type_name: String, + + /// A string specifying the visibility of the file set; one of PUBLIC, PRIVATE, or INTERFACE. + pub visibility: String, + + /// Base directories containing sources in the file set. + /// If the directory is inside the top-level source directory then the path is specified + /// relative to that directory. + /// Otherwise, the path is absolute. + pub base_directories: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Source { + /// Path to the source file on disk, represented with forward slashes. + /// If the file is inside the top-level source directory then the path is specified relative to that directory. + /// Otherwise the path is absolute. + pub path: PathBuf, + + /// Optional member that is present when the source is compiled. + /// The value is an unsigned integer 0-based index into the compileGroups array. + pub compile_group_index: Option, + + /// Optional member that is present when the source is part of a source group either via the source_group() command or by default. + /// The value is an unsigned integer 0-based index into the sourceGroups array. + pub source_group_index: Option, + + /// True if the source is GENERATED. + #[serde(default)] + pub is_generated: bool, + + /// Optional member that is present when the source is part of a file set. + /// The value is an unsigned integer 0-based index into the fileSets array. + /// This field was added in codemodel version 2.5. + pub file_set_index: Option, + + /// Optional member that is present when a CMake language backtrace to the target_sources(), add_executable(), add_library(), + /// add_custom_target(), or other command invocation that added this source to the target is available. + /// The value is an unsigned integer 0-based index into the backtraceGraph member's nodes array. + pub backtrace: Option, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct SourceGroup { + /// A string specifying the name of the source group. + pub name: String, + + /// Indices to sources belonging to the group. + /// Each entry is an unsigned integer 0-based index into the main sources array for the target. + pub source_indexes: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct CompileGroup { + /// Indices to sources belonging to the compile-group. + pub source_indexes: Vec, + + /// A string specifying the language (e.g. C, CXX, Fortran) of the toolchain is used to compile the source file. + pub language: String, + + /// Optional member that is present when the language standard is set explicitly (e.g. via CXX_STANDARD) or + /// implicitly by compile features. + /// This field was added in codemodel version 2.2. + pub language_standard: Option, + + /// Optional member that is present when fragments of the compiler command line invocation are available. + #[serde(default)] + pub compile_command_fragments: Vec, + + /// include directories. + #[serde(default)] + pub includes: Vec, + + /// available frameworks (Apple) + /// This field was added in codemodel version 2.6. + #[serde(default)] + pub frameworks: Vec, + + /// precompiled headers + #[serde(default)] + pub precompile_headers: Vec, + + /// defines + #[serde(default)] + pub defines: Vec, + + /// Optional member that is present when the `CMAKE_SYSROOT_COMPILE` or `CMAKE_SYSROOT` variable is defined. + pub sysroot: Option, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct LanguageStandard { + /// Optional member that is present when a CMake language backtrace to the `_STANDARD` setting is available. + /// If the language standard was set implicitly by compile features those are used as the backtrace(s). + /// It's possible for multiple compile features to require the same language standard so there could be multiple backtraces. + /// Each entry being an unsigned integer 0-based index into the backtraceGraph member's nodes array. + #[serde(default)] + pub backtraces: Vec, + + /// String representing the language standard. + pub standard: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct CompileCommandFragment { + /// A string specifying a fragment of the compile command line invocation. + /// The value is encoded in the build system's native shell format. + pub fragment: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Include { + /// Path to the include directory, represented with forward slashes. + pub path: PathBuf, + + /// True if the include directory is marked as a system include directory. + #[serde(default)] + pub is_system: bool, + + /// Optional member that is present when a CMake language backtrace to the target_include_directories() or + /// other command invocation that added this include directory is available. + /// The value is an unsigned integer 0-based index into the backtraceGraph member's nodes array. + pub backtrace: Option, +} +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Framework { + /// Path to the framework directory, represented with forward slashes. + pub path: PathBuf, + + /// True if the framework is marked as a system one. + #[serde(default)] + pub is_system: bool, + + /// Optional member that is present when a CMake language backtrace to the target_link_libraries() or + /// other command invocation that added this framework is available. + /// The value is an unsigned integer 0-based index into the backtraceGraph member's nodes array. + pub backtrace: Option, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct PrecompileHeader { + /// Full path to the precompile header file. + pub header: PathBuf, + + /// Optional member that is present when a CMake language backtrace to the target_precompile_headers() or + /// other command invocation that added this precompiled header is available. + /// The value is an unsigned integer 0-based index into the backtraceGraph member's nodes array. + pub backtrace: Option, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Define { + /// A string specifying the preprocessor definition in the format `[=]`, e.g. `DEF` or `DEF=1`. + pub define: String, + + /// Optional member that is present when a CMake language backtrace to the target_compile_definitions() or + /// other command invocation that added this preprocessor definition is available. + /// The value is an unsigned integer 0-based index into the backtraceGraph member's nodes array. + pub backtrace: Option, +} + +impl CompileGroup { + /// Returns a list of defines for the compile group + /// + /// Compile command fragments can contain defines as well (/D or -D). + #[must_use] + pub fn defines(&self) -> Vec { + let mut defines: Vec = self + .defines + .iter() + .map(|define| define.define.clone()) + .collect(); + defines.extend(self.compile_fragments().iter().filter_map(|flag| { + if Self::is_define(flag) { + flag.get(2..).map(|define| define.to_owned()) + } else { + None + } + })); + defines + } + + /// Returns a list of compile flags for the compile group + /// + /// Compile command fragments are split into single flags and defines (/D or -D) are filtered out. + #[must_use] + pub fn flags(&self) -> Vec { + self.compile_fragments() + .iter() + .filter(|&flag| !Self::is_define(flag)) + .cloned() + .collect() + } + + fn is_define(flag: &str) -> bool { + flag.starts_with("/D") || flag.starts_with("-D") + } + + #[must_use] + /// Compile command fragments are split into single flags. + pub fn compile_fragments(&self) -> Vec { + self.compile_command_fragments + .iter() + .filter_map(|frag| shlex::split(&frag.fragment)) + .flatten() + .collect() + } +} + +#[cfg(test)] +mod tests { + use crate::objects::codemodel_v2::target::*; + use crate::objects::codemodel_v2::Node; + use serde_json::json; + + #[test] + fn test_target() { + let json = json!({ + "backtrace" : 0, + "backtraceGraph" : + { + "commands" : [], + "files" : + [ + "CMakeLists.txt" + ], + "nodes" : + [ + { + "file" : 0 + } + ] + }, + "dependencies" : + [ + { + "id" : "ZERO_CHECK::@6890427a1f51a3e7e1df" + }, + { + "id" : "subbinary::@6890427a1f51a3e7e1df" + } + ], + "id" : "ALL_BUILD::@6890427a1f51a3e7e1df", + "isGeneratorProvided" : true, + "name" : "ALL_BUILD", + "paths" : + { + "build" : ".", + "source" : "." + }, + "sources" : [], + "type" : "UTILITY" + } + ); + + let target = serde_json::from_value::(json).unwrap(); + assert_eq!( + target, + Target { + backtrace: Some(0), + backtrace_graph: BacktraceGraph { + commands: vec![], + files: vec!["CMakeLists.txt".into()], + nodes: vec![Node { + file: 0, + ..Default::default() + }] + }, + dependencies: vec![ + Dependency { + id: "ZERO_CHECK::@6890427a1f51a3e7e1df".to_string(), + ..Default::default() + }, + Dependency { + id: "subbinary::@6890427a1f51a3e7e1df".to_string(), + ..Default::default() + } + ], + id: "ALL_BUILD::@6890427a1f51a3e7e1df".to_string(), + is_generator_provided: true, + name: "ALL_BUILD".to_string(), + paths: TargetPaths { + build: ".".into(), + source: ".".into() + }, + sources: vec![], + type_name: "UTILITY".to_string(), + ..Default::default() + } + ); + } +} diff --git a/src/objects/configure_log_v1.rs b/src/objects/configure_log_v1.rs new file mode 100644 index 0000000..2368f5e --- /dev/null +++ b/src/objects/configure_log_v1.rs @@ -0,0 +1,74 @@ +use crate::objects::{MajorMinor, Object, ObjectKind}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +/// The configureLog object kind describes the location and contents of a cmake-configure-log(7) file. +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct ConfigureLog { + /// Kind of the configureLog object. + pub kind: ObjectKind, + + /// Version of the configureLog object. + pub version: MajorMinor, + + /// Path to the configure log file. + /// Clients must read the log file from this path, which may be different from the path documented by cmake-configure-log(7). + /// The log file may not exist if no events are logged. + pub path: PathBuf, + + /// Names of the event kinds that are logged in the configure log. + pub event_kind_names: Vec, +} + +impl Object for ConfigureLog { + fn kind() -> ObjectKind { + ObjectKind::ConfigureLog + } + + fn major() -> u32 { + 1 + } +} + +#[cfg(test)] +mod tests { + use crate::objects::configure_log_v1::*; + use crate::objects::MajorMinor; + use serde_json::json; + + #[test] + fn test_configure_log() { + let json = json!({ + "kind" : "configureLog", + "path" : "build/CMakeFiles/CMakeConfigureLog.yaml", + "version" : + { + "major" : 1, + "minor" : 0 + }, + "eventKindNames" : + [ + "message-v1", + "try_compile-v1", + "try_run-v1" + ] + }); + + let configure_log = serde_json::from_value::(json).unwrap(); + assert_eq!( + configure_log, + ConfigureLog { + event_kind_names: vec![ + "message-v1".into(), + "try_compile-v1".into(), + "try_run-v1".into() + ], + kind: ObjectKind::ConfigureLog, + path: "build/CMakeFiles/CMakeConfigureLog.yaml".into(), + version: MajorMinor { major: 1, minor: 0 } + } + ); + } +} diff --git a/src/objects/toolchains_v1.rs b/src/objects/toolchains_v1.rs new file mode 100644 index 0000000..1f95fbf --- /dev/null +++ b/src/objects/toolchains_v1.rs @@ -0,0 +1,184 @@ +use crate::objects::{MajorMinor, Object, ObjectKind}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +/// The toolchains object kind lists properties of the toolchains used during the build +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Toolchains { + /// Kind of the toolchains object. + pub kind: ObjectKind, + + /// Version of the toolchains object. + pub version: MajorMinor, + + /// Toolchains. + pub toolchains: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Toolchain { + /// Toolchain language, like C or CXX. + pub language: String, + + /// Compiler information. + pub compiler: Compiler, + + /// Optional member that is present when the `CMAKE__SOURCE_FILE_EXTENSIONS` variable is defined for the current language. + /// Each string holds a file extension (without the leading dot) for the language + #[serde(default)] + pub source_file_extensions: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Compiler { + /// Optional member that is present when the `CMAKE__COMPILER` variable is defined for the current language. + /// Holding the absolute path to the compiler. + pub path: Option, + + /// Optional member that is present when the `CMAKE__COMPILER_ID` variable is defined for the current language. + /// Holding the ID (GNU, MSVC, etc.) of the compiler. + pub id: Option, + + /// Optional member that is present when the `CMAKE__COMPILER_VERSION` variable is defined for the current language. + /// Holding the version of the compiler. + pub version: Option, + + /// Optional member that is present when the `CMAKE__COMPILER_TARGET` variable is defined for the current language. + /// Holding the cross-compiling target of the compiler. + pub target: Option, + + /// Implicit compiler info for `CMAKE__IMPLICIT_*` variables. + pub implicit: Implicit, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Implicit { + /// Optional member that is present when the `CMAKE__IMPLICIT_INCLUDE_DIRECTORIES` variable is defined for the current language. + /// Each path points to an implicit include directory for the compiler. + #[serde(default)] + pub include_directories: Vec, + + /// Optional member that is present when the `CMAKE__IMPLICIT_LINK_DIRECTORIES` variable is defined for the current language. + /// Each path points to an implicit link directory for the compiler. + #[serde(default)] + pub link_directories: Vec, + + /// Optional member that is present when the `CMAKE__IMPLICIT_LINK_FRAMEWORK_DIRECTORIES` variable is defined for the current language. + /// Each path points to an implicit link framework directory for the compiler. + #[serde(default)] + pub link_framework_directories: Vec, + + /// Optional member that is present when the `CMAKE__IMPLICIT_LINK_LIBRARIES` variable is defined for the current language. + /// Each path points to an implicit link library for the compiler. + #[serde(default)] + pub link_libraries: Vec, +} + +impl Object for Toolchains { + fn kind() -> ObjectKind { + ObjectKind::Toolchains + } + + fn major() -> u32 { + 1 + } +} + +#[cfg(test)] +mod tests { + use crate::objects::toolchains_v1::*; + use serde_json::json; + + #[test] + fn test_toolchains() { + let json = json!({ + "kind": "toolchains", + "version": { "major": 1, "minor": 0 }, + "toolchains": [ + { + "language": "C", + "compiler": { + "path": "/usr/bin/cc", + "id": "GNU", + "version": "9.3.0", + "implicit": { + "includeDirectories": [ + "/usr/lib/gcc/x86_64-linux-gnu/9/include", + "/usr/local/include", + "/usr/include/x86_64-linux-gnu", + "/usr/include" + ], + "linkDirectories": [ + "/usr/lib/gcc/x86_64-linux-gnu/9", + "/usr/lib/x86_64-linux-gnu", + "/usr/lib", + "/lib/x86_64-linux-gnu", + "/lib" + ], + "linkFrameworkDirectories": [], + "linkLibraries": [ "gcc", "gcc_s", "c", "gcc", "gcc_s" ] + } + }, + "sourceFileExtensions": [ "c", "m" ] + }, + { + "language": "CXX", + "compiler": { + "path": "/usr/bin/c++", + "id": "GNU", + "version": "9.3.0", + "implicit": { + "includeDirectories": [ + "/usr/include/c++/9", + "/usr/include/x86_64-linux-gnu/c++/9", + "/usr/include/c++/9/backward", + "/usr/lib/gcc/x86_64-linux-gnu/9/include", + "/usr/local/include", + "/usr/include/x86_64-linux-gnu", + "/usr/include" + ], + "linkDirectories": [ + "/usr/lib/gcc/x86_64-linux-gnu/9", + "/usr/lib/x86_64-linux-gnu", + "/usr/lib", + "/lib/x86_64-linux-gnu", + "/lib" + ], + "linkFrameworkDirectories": [], + "linkLibraries": [ + "stdc++", "m", "gcc_s", "gcc", "c", "gcc_s", "gcc" + ] + } + }, + "sourceFileExtensions": [ + "C", "M", "c++", "cc", "cpp", "cxx", "mm", "CPP" + ] + } + ] + }); + + let toolchains = serde_json::from_value::(json).unwrap(); + assert_eq!(toolchains.kind, ObjectKind::Toolchains); + assert_eq!(toolchains.version, MajorMinor { major: 1, minor: 0 }); + assert_eq!(toolchains.toolchains.len(), 2); + assert_eq!(toolchains.toolchains[0].language, "C"); + assert_eq!(toolchains.toolchains[1].language, "CXX"); + + assert_eq!( + toolchains.toolchains[0].compiler.id.as_ref().unwrap(), + "GNU" + ); + assert_eq!( + toolchains.toolchains[1].compiler.id.as_ref().unwrap(), + "GNU" + ); + } +} diff --git a/src/query.rs b/src/query.rs new file mode 100644 index 0000000..f913c2f --- /dev/null +++ b/src/query.rs @@ -0,0 +1,176 @@ +use crate::objects; +use crate::objects::ObjectKind; +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; +use std::{fs, io}; + +/// Errors for writing queries +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum WriterError { + #[error("IO error: {0}")] + IO(io::Error), + + #[error("Failed to serialize query: {0}")] + Parse(serde_json::Error), + + #[error("Client name not set")] + ClientNameNotSet, +} + +impl From for WriterError { + fn from(err: io::Error) -> Self { + WriterError::IO(err) + } +} + +impl From for WriterError { + fn from(err: serde_json::Error) -> Self { + WriterError::Parse(err) + } +} + +/// Write queries for cmake-file-api. +/// +/// # Example +/// +/// ```no_run +/// use cmake_file_api::{query, objects}; +/// # let build_dir = std::path::Path::new("."); +/// +/// query::Writer::default() +/// .request_object::() +/// .write_stateless(&build_dir) +/// .expect("Failed to write query"); +/// ``` +#[derive(Default)] +pub struct Writer { + query: Query, + client_name: Option, +} + +impl Writer { + /// Request cmake-file-api object + pub fn request_object(&mut self) -> &mut Self { + self.query.requests.push(Request { + kind: T::kind(), + version: OptionalVersion { + major: T::major(), + minor: None, + }, + }); + self + } + + /// Request cmake-file-api object with exact version (minor version only used for stateful queries) + pub fn add_request_exact(&mut self, minor: u32) -> &mut Self { + self.query.requests.push(Request { + kind: T::kind(), + version: OptionalVersion { + major: T::major(), + minor: Some(minor), + }, + }); + self + } + + /// Helper function to request all objects + pub fn request_all_objects(&mut self) -> &mut Self { + self.request_object::() + .request_object::() + .request_object::() + .request_object::() + .request_object::() + } + + /// Set client data + /// Only used for stateful queries + /// + /// # Arguments + /// + /// * `client_name` - Client name + /// * `client_data` - Client data (JSON) + pub fn set_client(&mut self, client_name: &str, client_data: serde_json::Value) -> &mut Self { + self.query.client = Some(client_data); + self.client_name = Some(client_name.to_owned()); + self + } + + /// Write stateless query + /// For every object requested, a file is created in the query folder e.g. `/.cmake/api/v1/query/codemodel-v2` + /// + /// # Errors + /// + /// Returns an error if the query folder could not be created + /// Returns an error if the query file could not be written + pub fn write_stateless>(&self, build_dir: P) -> Result<(), WriterError> { + let query_dir = dir(build_dir); + + // create query folder + fs::create_dir_all(&query_dir)?; + + for obj in &self.query.requests { + let query_file = + query_dir.join(format!("{}-v{}", obj.kind.as_str(), obj.version.major)); + fs::write(&query_file, "")?; + } + + Ok(()) + } + + /// Write stateful query + /// A single `/query.json` file is created in the query folder containing all requested objects and when set the client data + /// + /// # Arguments + /// + /// * `build_dir` - Build directory + /// + /// # Errors + /// + /// Returns an error if the query file could not be written + pub fn write_stateful>(&self, build_dir: P) -> Result<(), WriterError> { + let query_dir = dir(build_dir); + let client_dir = query_dir.join( + self.client_name + .as_ref() + .ok_or(WriterError::ClientNameNotSet)?, + ); + + // create query folder + fs::create_dir_all(&client_dir)?; + + // create query file + let query_file = client_dir.join("query.json"); + let query = serde_json::to_string(&self.query)?; + fs::write(query_file, query)?; + + Ok(()) + } +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +struct OptionalVersion { + major: u32, + #[serde(skip_serializing_if = "Option::is_none")] + minor: Option, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +struct Request { + kind: ObjectKind, + version: OptionalVersion, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +struct Query { + requests: Vec, + client: Option, +} +/// Get query folder for a given build directory +pub fn dir>(build_dir: P) -> PathBuf { + Path::new(build_dir.as_ref()) + .join(".cmake") + .join("api") + .join("v1") + .join("query") +} diff --git a/src/reply.rs b/src/reply.rs new file mode 100644 index 0000000..d1b7fe1 --- /dev/null +++ b/src/reply.rs @@ -0,0 +1,170 @@ +use crate::{index, objects, reply}; +use serde::de::DeserializeOwned; +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; +use std::{fs, io}; + +/// Errors for reading replies +#[non_exhaustive] +#[derive(Debug, thiserror::Error)] +pub enum ReaderError { + #[error("IO error: {0}")] + IO(io::Error), + + #[error("Failed to deserialize reply: {0}")] + Parse(serde_json::Error), + + #[error("cmake-file-api is not generated for this build directory")] + FileApiNotGenerated, + + #[error("failed to find object")] + ObjectNotFound, +} + +impl From for ReaderError { + fn from(err: io::Error) -> Self { + ReaderError::IO(err) + } +} + +impl From for ReaderError { + fn from(err: serde_json::Error) -> Self { + ReaderError::Parse(err) + } +} + +/// Reader for cmake-file-api replies +/// +/// Example: +/// +/// ```no_run +/// use cmake_file_api::{query, objects}; +/// # let build_dir = std::path::Path::new("."); +/// +/// query::Writer::default() +/// .request_object::() +/// .write_stateless(&build_dir) +/// .expect("Failed to write query"); +/// ``` +pub struct Reader { + /// Build directory + build_dir: PathBuf, + + /// Index file + index: index::Index, +} + +impl Reader { + /// Create a new reader from a build directory + /// + /// # Errors + /// + /// `ReaderError::FileApiNotGenerated`: if the cmake-file-api is not generated for the build directory + /// `ReaderError::IO`: if an IO error occurs while reading the index file + /// `ReaderError::Parse`: if an error occurs while parsing the index file + pub fn from_build_dir>(build_dir: P) -> Result { + let index_file = index_file(build_dir.as_ref()).ok_or(ReaderError::FileApiNotGenerated)?; + let index = Reader::parse_reply(index_file)?; + Ok(Reader { + build_dir: build_dir.as_ref().to_path_buf(), + index, + }) + } + + #[must_use] + pub fn build_dir(&self) -> &Path { + &self.build_dir + } + + #[must_use] + pub fn index(&self) -> &index::Index { + &self.index + } + + #[must_use] + pub fn has_object(&self) -> bool { + self.find_object(T::kind(), T::major()).is_some() + } + + /// read object + /// + /// # Errors + /// + /// `ReaderError::ObjectNotFound`: if the index file does not contain the requested object + /// `ReaderError::IO`: if an IO error occurs while reading the object file + /// `ReaderError::Parse`: if an error occurs while parsing the object file + pub fn read_object(&self) -> Result { + let reply_reference = self + .find_object(T::kind(), T::major()) + .ok_or(ReaderError::ObjectNotFound)?; + let reply_file = reply::dir(&self.build_dir).join(&reply_reference.json_file); + let mut object: T = Reader::parse_reply(reply_file)?; + + object.resolve_references(self)?; + + Ok(object) + } + + /// Parse a reply file into a given object type + pub(crate) fn parse_reply, Object: DeserializeOwned>( + reply_file: P, + ) -> Result { + let content = fs::read_to_string(&reply_file)?; + + let object = serde_json::from_str(content.as_str())?; + + Ok(object) + } + + /// Find an object in the index file + fn find_object( + &self, + kind: objects::ObjectKind, + major: u32, + ) -> Option<&index::ReplyFileReference> { + self.index + .objects + .iter() + .find(|obj| obj.kind == kind && obj.version.major == major) + } +} + +/// Get cmake-file-api reply path for a given build directory +pub fn dir>(build_dir: P) -> PathBuf { + Path::new(build_dir.as_ref()) + .join(".cmake") + .join("api") + .join("v1") + .join("reply") +} + +/// Get cmake-file-api index file path for a given build directory +pub fn index_file>(build_dir: P) -> Option { + let reply_dir = dir(build_dir); + + if !reply_dir.exists() { + return None; + } + + // find json file with 'index-' prefix + fs::read_dir(&reply_dir).ok()?.find_map(|entry| { + let path = entry.ok()?.path(); + if path.is_file() { + if let Some(file_name) = path.file_name().and_then(OsStr::to_str) { + if file_name.starts_with("index-") + && path + .extension() + .map_or(false, |ext| ext.eq_ignore_ascii_case("json")) + { + return Some(path); + } + } + } + None + }) +} + +/// Check if cmake-file-api is available for a given build directory +pub fn is_available>(build_dir: P) -> bool { + index_file(build_dir).is_some() +} diff --git a/tests/test_query.rs b/tests/test_query.rs new file mode 100644 index 0000000..6587d45 --- /dev/null +++ b/tests/test_query.rs @@ -0,0 +1,90 @@ +use cmake_file_api::objects; + +#[test] +fn query_writer_write_stateless_creates_files() { + let tmp_dir = tempdir::TempDir::new("test_cmake").unwrap(); + let build_dir = tmp_dir.path(); + + cmake_file_api::query::Writer::default() + .request_object::() + .request_object::() + .request_object::() + .request_object::() + .request_object::() + .write_stateless(build_dir) + .unwrap(); + + assert!( + cmake_file_api::query::dir(build_dir) + .join("codemodel-v2") + .exists(), + "codeModel-v2 should exist" + ); + assert!( + cmake_file_api::query::dir(build_dir) + .join("configureLog-v1") + .exists(), + "configureLog-v1 should exist" + ); + assert!( + cmake_file_api::query::dir(build_dir) + .join("cache-v2") + .exists(), + "cache-v2 should exist" + ); + assert!( + cmake_file_api::query::dir(build_dir) + .join("toolchains-v1") + .exists(), + "toolchains-v1 should exist" + ); + assert!( + cmake_file_api::query::dir(build_dir) + .join("cmakeFiles-v1") + .exists(), + "cmakeFiles-v1 should exist" + ); +} + +#[test] +fn query_writer_write_statefull_creates_files() { + let tmp_dir = tempdir::TempDir::new("test_cmake").unwrap(); + let build_dir = tmp_dir.path(); + + cmake_file_api::query::Writer::default() + .set_client("test_client", serde_json::json!({"my_key": "my_value"})) + .request_object::() + .request_object::() + .request_object::() + .request_object::() + .request_object::() + .write_stateful(build_dir) + .unwrap(); + + let client_dir = cmake_file_api::query::dir(build_dir).join("test_client"); + let query_file = client_dir.join("query.json"); + assert!(query_file.exists(), "query file should exist"); + + let query = std::fs::read_to_string(&query_file).expect("query file should be readable"); + let query_json: serde_json::Value = serde_json::from_str(&query).expect("query should be json"); + + // should contain client data + assert_eq!( + query_json["client"], + serde_json::json!({"my_key": "my_value"}), + "client data should be written" + ); + + // should contain requested objects + assert_eq!( + query_json["requests"], + serde_json::json!([ + {"kind": "codemodel", "version": {"major": 2}}, + {"kind": "configureLog", "version": {"major": 1}}, + {"kind": "cache", "version": {"major": 2}}, + {"kind": "toolchains", "version": {"major": 1}}, + {"kind": "cmakeFiles", "version": {"major": 1}}, + ]), + "requests should be written for each object" + ); +} diff --git a/tests/test_real_projects.rs b/tests/test_real_projects.rs new file mode 100644 index 0000000..ab9ab5a --- /dev/null +++ b/tests/test_real_projects.rs @@ -0,0 +1,191 @@ +use cmake_file_api::{objects, reply}; +use std::path::Path; +use std::process::Command; + +fn validate_cmake_file_api>(build_dir: P) { + // Test that the API is available + assert!(reply::is_available(&build_dir)); + + // Test that the index_file function returns the index file + assert!(reply::index_file(&build_dir) + .expect("index file should be available") + .is_file()); + + // Test that the CMakeFileApi::from_build_dir function returns the CMakeFileApi object + let reader = reply::Reader::from_build_dir(&build_dir).expect("CMakeFileApi should be created"); + + // Test that the CMakeFileApi object can be used to get the configure log + assert!( + reader.has_object::(), + "configure log should be available" + ); + + // Test that the CMakeFileApi object can be used to get the cache + assert!( + reader.has_object::(), + "cache should be available" + ); + + // Test that the CMakeFileApi object can be used to get the toolchains + assert!( + reader.has_object::(), + "toolchains should be available" + ); + + // Test that the CMakeFileApi object can be used to get the cmake files + assert!( + reader.has_object::(), + "cmake files should be available" + ); + + // Test that the CMakeFileApi object can be used to get the codemodel + let codemodel: objects::CodeModelV2 = reader.read_object().expect("codemodel should be available"); + for config in &codemodel.configurations { + for target in &config.targets { + println!("{}", target.name); + println!("{:#?}", target.sources); + } + } +} + +#[test] +#[ignore] +fn test_llvm() { + let tmp_dir = tempdir::TempDir::new("llvm").unwrap(); + let checkout_dir = tmp_dir.path(); + let cmake_source_dir = checkout_dir.join("llvm"); + let build_dir = checkout_dir.join("build"); + + // clone llvm + Command::new("git") + .arg("clone") + .arg("--depth") + .arg("1") + .arg("--branch") + .arg("llvmorg-18.1.8") + .arg("https://github.com/llvm/llvm-project.git") + .arg(checkout_dir) + .output() + .expect("failed to clone llvm"); + + // create build directory + std::fs::create_dir(&build_dir).expect("failed to create build directory"); + + // write query + cmake_file_api::query::Writer::default() + .request_object::() + .request_object::() + .request_object::() + .request_object::() + .request_object::() + .write_stateless(&build_dir) + .expect("failed to write query"); + + // run cmake + assert!(Command::new("cmake") + .arg("-S") + .arg(cmake_source_dir) + .arg("-B") + .arg(&build_dir) + .arg("-G") + .arg("Ninja") + .arg("-DCMAKE_BUILD_TYPE=Debug") + .status() + .expect("failed to run cmake") + .success()); + + // test api + validate_cmake_file_api(&build_dir); +} + +#[test] +#[ignore] +fn test_abseil() { + let tmp_dir = tempdir::TempDir::new("abseil").unwrap(); + let checkout_dir = tmp_dir.path(); + let cmake_source_dir = checkout_dir.join("abseil-cpp"); + let build_dir = checkout_dir.join("build"); + + // clone abseil + Command::new("git") + .arg("clone") + .arg("--depth") + .arg("1") + .arg("--branch") + .arg("20240722.0") + .arg("https://github.com/abseil/abseil-cpp.git") + .arg(&cmake_source_dir) + .output() + .expect("failed to clone abseil"); + + // create build directory + std::fs::create_dir(&build_dir).expect("failed to create build directory"); + + // write query + cmake_file_api::query::Writer::default() + .request_all_objects() + .write_stateless(&build_dir) + .expect("failed to write query"); + + // run cmake + assert!(Command::new("cmake") + .arg("-S") + .arg(&cmake_source_dir) + .arg("-B") + .arg(&build_dir) + .arg("-G") + .arg("Ninja") + .arg("-DCMAKE_BUILD_TYPE=Debug") + .status() + .expect("failed to run cmake") + .success()); + + // test api + validate_cmake_file_api(&build_dir); +} + +#[test] +#[ignore] +fn test_googletest() { + let tmp_dir = tempdir::TempDir::new("googletest").unwrap(); + let checkout_dir = tmp_dir.path(); + let cmake_source_dir = checkout_dir.join("googletest"); + let build_dir = checkout_dir.join("build"); + + // clone googletest + Command::new("git") + .arg("clone") + .arg("--depth") + .arg("1") + .arg("--branch") + .arg("v1.15.2") + .arg("https://github.com/google/googletest.git") + .arg(&cmake_source_dir) + .output() + .expect("failed to clone googletest"); + + // create build directory + std::fs::create_dir(&build_dir).expect("failed to create build directory"); + + // write query + cmake_file_api::query::Writer::default() + .request_all_objects() + .write_stateless(&build_dir) + .expect("failed to write query"); + + // run cmake + assert!(Command::new("cmake") + .arg("-S") + .arg(&cmake_source_dir) + .arg("-B") + .arg(&build_dir) + .arg("-G") + .arg("Ninja") + .arg("-DCMAKE_BUILD_TYPE=Debug") + .status() + .expect("failed to run cmake") + .success()); + + // test api + validate_cmake_file_api(&build_dir); +} diff --git a/tests/test_reply.rs b/tests/test_reply.rs new file mode 100644 index 0000000..0f0ce78 --- /dev/null +++ b/tests/test_reply.rs @@ -0,0 +1,178 @@ +use cmake_file_api::{objects, reply}; + +#[test] +fn test_missing_api() { + let tmp_dir = tempdir::TempDir::new("test_cmake").unwrap(); + let empty_dir = tmp_dir.path(); + + // Test that the API is not available when the directory is empty + assert_eq!(reply::is_available(&empty_dir), false); + + // Test that the index_file function returns None when the directory is empty + assert!(reply::index_file(&empty_dir).is_none()); + + // Test for cmake_file_api::CMakeFileApiError::FileApiNotGenerated + assert!(matches!( + reply::Reader::from_build_dir(&empty_dir), + Err(reply::ReaderError::FileApiNotGenerated) + )); +} + +#[test] +fn test_json_parser_error() { + let tmp_dir = tempdir::TempDir::new("test_cmake").unwrap(); + let build_dir = tmp_dir.path(); + + // create empty reply dir + std::fs::create_dir_all(&reply::dir(&build_dir)).unwrap(); + + // create broken index file + let broken_index_file = reply::dir(&build_dir).join("index-broken.json"); + std::fs::write(&broken_index_file, "broken").unwrap(); + + // Test that the API is available when the reply directory exists + assert_eq!(reply::is_available(&build_dir), true); + + // Test that the index_file function returns None when the index file is missing + assert_eq!( + reply::index_file(&build_dir), + Some(broken_index_file.clone()) + ); + + // Test ReaderError::Parse + assert!(matches!( + reply::Reader::from_build_dir(&build_dir), + Err(reply::ReaderError::Parse(_)) + )); +} + +/// +#[test] +fn test_valid_api() { + let tmp_dir = tempdir::TempDir::new("test_cmake").unwrap(); + let project_dir = tmp_dir.path(); + + // create minimal main.cpp + { + let main_cpp = project_dir.join("main.cpp"); + std::fs::write(&main_cpp, "int main() { return 0; }").expect("Failed to write main.cpp"); + } + + // create libfoo library + { + let libfoo_dir = project_dir.join("libfoo"); + std::fs::create_dir(&libfoo_dir).unwrap(); + + let libfoo_cpp = libfoo_dir.join("libfoo.cpp"); + std::fs::write( + libfoo_cpp, + r#" + extern int foo(); + "#, + ) + .expect("Failed to write libfoo.cpp"); + + let libfoo_h = libfoo_dir.join("libfoo.h"); + std::fs::write( + libfoo_h, + r#" + #include + int foo() { return 42; } + "#, + ) + .expect("Failed to write libfoo.h"); + + let cmake_lists = libfoo_dir.join("CMakeLists.txt"); + std::fs::write( + cmake_lists, + r#" + add_library(foo libfoo.cpp libfoo.h) + target_include_directories(foo PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + "#, + ) + .expect("Failed to write CMakeLists.txt"); + } + + // create minimal CMakeLists.txt + { + let cmake_lists = project_dir.join("CMakeLists.txt"); + std::fs::write( + cmake_lists, + r#" + cmake_minimum_required(VERSION 3.10) + project(test_cmake) + + add_subdirectory(libfoo) + + add_executable(test_cmake main.cpp) + target_link_libraries(test_cmake foo) + "#, + ) + .expect("Failed to write CMakeLists.txt"); + } + + let build_dir = tmp_dir.path().join("build"); + + // make query + cmake_file_api::query::Writer::default() + .request_all_objects() + .write_stateless(&build_dir) + .expect("Failed to write query"); + + // run cmake + assert!(std::process::Command::new("cmake") + .arg("-S") + .arg(&project_dir) + .arg("-B") + .arg(&build_dir) + .status() + .expect("Failed to run cmake") + .success()); + + // Test that the API is available + assert!(reply::is_available(&build_dir)); + + // Test that the index_file function returns the index file + assert!(reply::index_file(&build_dir).is_some()); + + // Test that the CMakeFileApi::from_build_dir function returns the CMakeFileApi object + let reader = + reply::Reader::from_build_dir(&build_dir).expect("Reply reader should be available"); + + // Test that the CMakeFileApi object can be used to get the CodeModel + assert!(reader.has_object::()); + + // Test that the CMakeFileApi object can be used to get the ConfigureLog + assert!(reader.has_object::()); + + // Test that the CMakeFileApi object can be used to get the Cache + assert!(reader.has_object::()); + + // Test that the CMakeFileApi object can be used to get the Toolchains + assert!(reader.has_object::()); + + // Test that the CMakeFileApi object can be used to get the CMakeFiles + assert!(reader.has_object::()); + + // Test that the CMakeFileApi object can be used to get the codemodel + let codemodel: objects::CodeModelV2 = reader.read_object().expect("codemodel should be available"); + assert!(codemodel.configurations.len() > 0); + + // targets should not be empty + assert!(codemodel.configurations[0].targets.len() > 0); + + // targets and target_refs should have the same length + assert_eq!( + codemodel.configurations[0].targets.len(), + codemodel.configurations[0].target_refs.len() + ); + + // directories should not be empty + assert!(codemodel.configurations[0].directories.len() > 0); + + // directories and directory_refs should have the same length + assert_eq!( + codemodel.configurations[0].directories.len(), + codemodel.configurations[0].directory_refs.len() + ); +}