use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use anyhow::Context;
use anyhow::Error;
use anyhow::Result;
use cargo_metadata::Metadata;
use cargo_metadata::MetadataCommand;
use cargo_metadata::Package;
use serde::Serialize;
use strum::AsRefStr;
use strum::Display;
use strum::EnumString;
use super::constants::BEVY_CRATE_NAME;
use super::constants::BEVY_REMOTE_FEATURE;
use super::constants::BEVY_REMOTE_GLOB_IMPORT_PREFIX;
use super::constants::BEVY_REMOTE_PLUGIN_IMPORT;
use super::constants::BEVY_REMOTE_REMOTE_GLOB_IMPORT_PREFIX;
use super::constants::BEVY_REMOTE_REMOTE_PLUGIN_IMPORT;
use super::constants::BRP_EXTRAS_GLOB_IMPORT_PREFIX;
use super::constants::BRP_EXTRAS_PLUGIN_IMPORT;
use super::constants::BRP_EXTRAS_PLUGIN_NAME;
use super::constants::CARGO_EXAMPLES_DIRECTORY;
use super::constants::CARGO_SRC_DIRECTORY;
use super::constants::MCP_CRATE_NAME;
use super::constants::REMOTE_PLUGIN_NAME;
use super::constants::RUST_SOURCE_EXTENSION;
use super::constants::TARGET_DIRECTORY_NAME;
use crate::app_tools::constants::CARGO_BIN_FLAG;
use crate::app_tools::constants::CARGO_EXAMPLE_FLAG;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, AsRefStr, EnumString, Serialize)]
#[strum(serialize_all = "lowercase")]
#[serde(rename_all = "lowercase")]
pub enum TargetType {
App,
Example,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub(super) enum BrpLevel {
None,
Core,
Extras,
}
impl BrpLevel {
pub(super) const fn as_str(self) -> &'static str {
match self {
Self::None => "none",
Self::Core => "brp_only",
Self::Extras => "extras",
}
}
}
impl TargetType {
pub fn add_cargo_args(self, command: &mut Command, target_name: &str) {
match self {
Self::App => {
command.arg(CARGO_BIN_FLAG).arg(target_name);
},
Self::Example => {
command.arg(CARGO_EXAMPLE_FLAG).arg(target_name);
},
}
}
}
#[derive(Debug, Clone)]
pub struct BevyTarget {
pub name: String,
pub target_type: TargetType,
pub package_name: String,
pub workspace_root: PathBuf,
pub manifest: PathBuf,
pub relative: PathBuf,
pub source: PathBuf,
}
impl BevyTarget {
pub fn get_binary_path(&self, profile: &str) -> PathBuf {
match self.target_type {
TargetType::App => self
.workspace_root
.join(TARGET_DIRECTORY_NAME)
.join(profile)
.join(&self.name),
TargetType::Example => self
.workspace_root
.join(TARGET_DIRECTORY_NAME)
.join(profile)
.join(CARGO_EXAMPLES_DIRECTORY)
.join(&self.name),
}
}
pub fn is_app(&self) -> bool { self.target_type == TargetType::App }
}
pub(super) struct CargoDetector {
metadata: Metadata,
}
impl TryFrom<&Path> for CargoDetector {
type Error = Error;
fn try_from(path: &Path) -> Result<Self> {
let metadata = MetadataCommand::new()
.current_dir(path)
.exec()
.context("Failed to execute cargo metadata")?;
Ok(Self { metadata })
}
}
impl CargoDetector {
fn is_workspace_member(&self, package: &Package) -> bool {
self.metadata.workspace_members.contains(&package.id)
}
fn find_packages_with_filter<'a, F>(&'a self, filter: F) -> impl Iterator<Item = &'a Package>
where
F: Fn(&Package) -> bool + 'a,
{
self.metadata
.packages
.iter()
.filter(|p| self.is_workspace_member(p))
.filter(move |p| filter(p))
}
fn extract_all_targets(&self, package: &Package) -> Vec<BevyTarget> {
let workspace_root: PathBuf = self.metadata.workspace_root.clone().into();
let package_name = package.name.to_string();
let manifest_path: PathBuf = package.manifest_path.clone().into();
let mut targets = Vec::new();
for target in package.targets.iter().filter(|t| t.is_bin()) {
targets.push(BevyTarget {
name: target.name.clone(),
target_type: TargetType::App,
package_name: package_name.clone(),
workspace_root: workspace_root.clone(),
manifest: manifest_path.clone(),
relative: PathBuf::new(), source: target.src_path.clone().into(),
});
}
for target in package.targets.iter().filter(|t| t.is_example()) {
targets.push(BevyTarget {
name: target.name.clone(),
target_type: TargetType::Example,
package_name: package_name.clone(),
workspace_root: workspace_root.clone(),
manifest: manifest_path.clone(),
relative: PathBuf::new(), source: target.src_path.clone().into(),
});
}
targets
}
fn bevy_app_filter(package: &Package) -> bool {
package.name.as_str() != MCP_CRATE_NAME
&& (package.name.as_str() == BEVY_CRATE_NAME || Self::package_depends_on_bevy(package))
}
fn brp_app_filter(package: &Package) -> bool {
package.name.as_str() != MCP_CRATE_NAME && Self::package_has_brp_support(package)
}
pub(super) fn find_bevy_targets(&self) -> Vec<BevyTarget> {
self.find_packages_with_filter(Self::bevy_app_filter)
.flat_map(|p| self.extract_all_targets(p))
.collect()
}
pub(super) fn find_brp_targets(&self) -> Vec<BevyTarget> {
self.find_packages_with_filter(Self::brp_app_filter)
.flat_map(|p| self.extract_all_targets(p))
.collect()
}
fn package_depends_on_bevy(package: &Package) -> bool {
package
.dependencies
.iter()
.any(|dependency| dependency.name == BEVY_CRATE_NAME)
}
fn package_has_brp_support(package: &Package) -> bool {
if !Self::package_has_bevy_remote_feature(package) {
return false;
}
Self::package_uses_brp_plugins(package)
}
fn package_has_bevy_remote_feature(package: &Package) -> bool {
package.dependencies.iter().any(|dependency| {
if dependency.name == BEVY_CRATE_NAME {
if dependency.features.is_empty() {
true
} else {
dependency
.features
.iter()
.any(|feature| feature == BEVY_REMOTE_FEATURE)
}
} else {
false
}
})
}
fn package_uses_brp_plugins(package: &Package) -> bool {
let Some(package_dir) = package.manifest_path.parent() else {
return false;
};
let src_dir = package_dir.join(CARGO_SRC_DIRECTORY);
if !src_dir.exists() {
return false;
}
Self::check_directory_for_brp_plugins(src_dir.as_std_path())
}
fn check_directory_for_brp_plugins(dir: &Path) -> bool {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if Self::check_directory_for_brp_plugins(&path) {
return true;
}
} else if path
.extension()
.is_some_and(|extension| extension == RUST_SOURCE_EXTENSION)
&& Self::file_uses_brp_plugins(&path)
{
return true;
}
}
}
false
}
pub(super) fn file_brp_level(file_path: &Path) -> BrpLevel {
let Ok(content) = std::fs::read_to_string(file_path) else {
return BrpLevel::None;
};
let has_extras = Self::content_has_extras_plugin(&content);
let has_remote = Self::content_has_remote_plugin(&content);
if has_extras {
BrpLevel::Extras
} else if has_remote {
BrpLevel::Core
} else {
BrpLevel::None
}
}
fn content_has_extras_plugin(content: &str) -> bool {
content.contains(BRP_EXTRAS_PLUGIN_IMPORT)
|| (content.contains(BRP_EXTRAS_GLOB_IMPORT_PREFIX)
&& content.contains(BRP_EXTRAS_PLUGIN_NAME))
}
fn content_has_remote_plugin(content: &str) -> bool {
content.contains(BEVY_REMOTE_PLUGIN_IMPORT)
|| (content.contains(BEVY_REMOTE_GLOB_IMPORT_PREFIX)
&& content.contains(REMOTE_PLUGIN_NAME))
|| content.contains(BEVY_REMOTE_REMOTE_PLUGIN_IMPORT)
|| (content.contains(BEVY_REMOTE_REMOTE_GLOB_IMPORT_PREFIX)
&& content.contains(REMOTE_PLUGIN_NAME))
}
pub(super) fn file_uses_brp_plugins(file_path: &Path) -> bool {
!matches!(Self::file_brp_level(file_path), BrpLevel::None)
}
}