pop-common 0.14.0

Library that provides a collection of essential utilities and shared functionality for pop.
Documentation
// SPDX-License-Identifier: GPL-3.0

use crate::Error;
use anyhow;
pub use cargo_toml::{Dependency, LtoSetting, Manifest, Profile, Profiles};
use glob::glob;
use std::{
	fs::write,
	path::{Path, PathBuf},
};

/// Parses the contents of a `Cargo.toml` manifest.
///
/// # Arguments
/// * `path` - The optional path to the manifest, defaulting to the current directory if not
///   specified.
pub fn from_path(path: &Path) -> Result<Manifest, Error> {
	// Resolve manifest path
	let path = match path.ends_with("Cargo.toml") {
		true => path.to_path_buf(),
		false => path.join("Cargo.toml"),
	};
	if !path.is_file() {
		return Err(Error::ManifestPath(path.display().to_string()));
	}
	Ok(Manifest::from_path(path.canonicalize()?)?)
}

/// Get the names and paths of all cargo projects that are associated with a workspace manifest.
///
/// # Arguments
/// * `manifest` - Path to the workspace manifest root folder.
pub fn get_workspace_project_names(project_path: &Path) -> Result<Vec<(String, PathBuf)>, Error> {
	let mut result = Vec::new();

	// Check if this is actually a workspace manifest
	let manifest = from_path(project_path)?;
	let workspace = manifest
		.workspace
		.as_ref()
		.ok_or_else(|| Error::Config("Manifest is not a workspace manifest".into()))?;

	// Get workspace members
	for member in &workspace.members {
		// Handle glob patterns in member paths
		for entry in glob(&project_path.join(member).to_string_lossy())
			.map_err(|e| Error::Config(format!("Invalid glob pattern '{}': {}", member, e)))?
			.filter_map(Result::ok)
		{
			let member_manifest_path = entry.join("Cargo.toml");
			if member_manifest_path.is_file() {
				// Parse the member's manifest to get its name
				if let Ok(member_manifest) = from_path(&member_manifest_path) &&
					let Some(package) = &member_manifest.package
				{
					result.push((package.name.clone(), entry));
				}
			}
		}
	}

	Ok(result)
}

/// Adds a "production" profile to the Cargo.toml manifest if it doesn't already exist.
///
/// # Arguments
/// * `project` - The path to the root of the Cargo project containing the Cargo.toml.
pub fn add_production_profile(project: &Path) -> anyhow::Result<()> {
	let root_toml_path = project.join("Cargo.toml");
	let mut manifest = Manifest::from_path(&root_toml_path)?;
	// Check if the `production` profile already exists.
	if manifest.profile.custom.contains_key("production") {
		return Ok(());
	}
	// Create the production profile with required fields.
	let production_profile = Profile {
		opt_level: None,
		debug: None,
		split_debuginfo: None,
		rpath: None,
		lto: Some(LtoSetting::Fat),
		debug_assertions: None,
		codegen_units: Some(1),
		panic: None,
		incremental: None,
		overflow_checks: None,
		strip: None,
		package: std::collections::BTreeMap::new(),
		build_override: None,
		inherits: Some("release".to_string()),
	};
	// Insert the new profile into the custom profiles
	manifest.profile.custom.insert("production".to_string(), production_profile);

	// Serialize the updated manifest and write it back to the file
	let toml_string = toml::to_string(&manifest)?;
	write(&root_toml_path, toml_string)?;

	Ok(())
}

/// Add a new feature to the Cargo.toml manifest if it doesn't already exist.
///
/// # Arguments
/// * `project` - The path to the project directory.
/// * `(key, items)` - The feature key and its associated items.
pub fn add_feature(project: &Path, (key, items): (String, Vec<String>)) -> anyhow::Result<()> {
	let root_toml_path = project.join("Cargo.toml");
	let mut manifest = Manifest::from_path(&root_toml_path)?;
	// Check if the feature already exists.
	if manifest.features.contains_key(&key) {
		return Ok(());
	}
	manifest.features.insert(key, items);

	// Serialize the updated manifest and write it back to the file
	let toml_string = toml::to_string(&manifest)?;
	write(&root_toml_path, toml_string)?;

	Ok(())
}

#[cfg(test)]
mod tests {
	use super::*;
	use std::fs::{File, read_to_string, write};
	use tempfile::TempDir;

	struct TestBuilder {
		main_tempdir: TempDir,
		workspace: Option<TempDir>,
		workspace_cargo_toml: Option<PathBuf>,
	}

	impl Default for TestBuilder {
		fn default() -> Self {
			Self {
				main_tempdir: TempDir::new().expect("Failed to create tempdir"),
				workspace: None,
				workspace_cargo_toml: None,
			}
		}
	}

	impl TestBuilder {
		fn add_workspace(self) -> Self {
			Self { workspace: TempDir::new_in(self.main_tempdir.as_ref()).ok(), ..self }
		}

		fn add_workspace_cargo_toml(self, cargo_toml_content: &str) -> Self {
			let workspace_cargo_toml = self
				.workspace
				.as_ref()
				.expect("add_workspace_cargo_toml is only callable if workspace has been created")
				.path()
				.join("Cargo.toml");
			File::create(&workspace_cargo_toml).expect("Failed to create Cargo.toml");
			write(&workspace_cargo_toml, cargo_toml_content).expect("Failed to write Cargo.toml");
			Self { workspace_cargo_toml: Some(workspace_cargo_toml.to_path_buf()), ..self }
		}
	}

	#[test]
	fn from_path_works() -> anyhow::Result<()> {
		// Workspace manifest from directory
		from_path(Path::new("../../"))?;
		// Workspace manifest from path
		from_path(Path::new("../../Cargo.toml"))?;
		// Package manifest from directory
		from_path(Path::new("."))?;
		// Package manifest from path
		from_path(Path::new("./Cargo.toml"))?;
		Ok(())
	}

	#[test]
	fn from_path_ensures_manifest_exists() -> Result<(), Error> {
		assert!(matches!(from_path(Path::new("./none.toml")), Err(super::Error::ManifestPath(..))));
		Ok(())
	}

	#[test]
	fn add_production_profile_works() {
		let test_builder = TestBuilder::default().add_workspace().add_workspace_cargo_toml(
			r#"[profile.release]
            opt-level = 3
            "#,
		);

		let binding = test_builder.workspace.expect("Workspace should exist");
		let project_path = binding.path();
		let cargo_toml_path = test_builder.workspace_cargo_toml.clone().unwrap();

		// Call the function to add the production profile
		let result = add_production_profile(project_path);
		assert!(result.is_ok());

		// Verify the production profile is added
		let manifest =
			Manifest::from_path(&cargo_toml_path).expect("Should parse updated Cargo.toml");
		let production_profile = manifest
			.profile
			.custom
			.get("production")
			.expect("Production profile should exist");
		assert_eq!(production_profile.codegen_units, Some(1));
		assert_eq!(production_profile.inherits.as_deref(), Some("release"));
		assert_eq!(production_profile.lto, Some(LtoSetting::Fat));

		// Test idempotency: Running the function again should not modify the manifest
		let initial_toml_content =
			read_to_string(&cargo_toml_path).expect("Cargo.toml should be readable");
		let second_result = add_production_profile(project_path);
		assert!(second_result.is_ok());
		let final_toml_content =
			read_to_string(&cargo_toml_path).expect("Cargo.toml should be readable");
		assert_eq!(initial_toml_content, final_toml_content);
	}

	#[test]
	fn add_feature_works() {
		let test_builder = TestBuilder::default().add_workspace().add_workspace_cargo_toml(
			r#"[profile.release]
            opt-level = 3
            "#,
		);

		let expected_feature_key = "runtime-benchmarks";
		let expected_feature_items =
			vec!["feature-a".to_string(), "feature-b".to_string(), "feature-c".to_string()];
		let binding = test_builder.workspace.expect("Workspace should exist");
		let project_path = binding.path();
		let cargo_toml_path = test_builder.workspace_cargo_toml.clone().unwrap();

		// Call the function to add the production profile
		let result = add_feature(
			project_path,
			(expected_feature_key.to_string(), expected_feature_items.clone()),
		);
		assert!(result.is_ok());

		// Verify the feature is added
		let manifest =
			Manifest::from_path(&cargo_toml_path).expect("Should parse updated Cargo.toml");
		let feature_items = manifest
			.features
			.get(expected_feature_key)
			.expect("Production profile should exist");
		assert_eq!(feature_items, &expected_feature_items);

		// Test idempotency: Running the function again should not modify the manifest
		let initial_toml_content =
			read_to_string(&cargo_toml_path).expect("Cargo.toml should be readable");
		let second_result = add_feature(
			project_path,
			(expected_feature_key.to_string(), expected_feature_items.clone()),
		);
		assert!(second_result.is_ok());
		let final_toml_content =
			read_to_string(&cargo_toml_path).expect("Cargo.toml should be readable");
		assert_eq!(initial_toml_content, final_toml_content);
	}

	#[test]
	fn get_workspace_project_names_works() {
		let test_builder = TestBuilder::default().add_workspace().add_workspace_cargo_toml(
			r#"[workspace]
members = ["crate1", "crate2"]

[workspace.package]
name = "test-workspace"
"#,
		);

		let binding = test_builder.workspace.expect("Workspace should exist");
		let workspace_path = binding.path();

		// Create member crates
		let crate1_path = workspace_path.join("crate1");
		std::fs::create_dir(&crate1_path).expect("Should create crate1 directory");
		write(
			crate1_path.join("Cargo.toml"),
			r#"[package]
name = "crate1"
version = "0.1.0"
"#,
		)
		.expect("Should write crate1 Cargo.toml");

		let crate2_path = workspace_path.join("crate2");
		std::fs::create_dir(&crate2_path).expect("Should create crate2 directory");
		write(
			crate2_path.join("Cargo.toml"),
			r#"[package]
name = "crate2"
version = "0.1.0"
"#,
		)
		.expect("Should write crate2 Cargo.toml");

		let result = get_workspace_project_names(workspace_path).expect("Should succeed");
		assert_eq!(result.len(), 2);

		// Check that both crates are found
		let names: Vec<String> = result.iter().map(|(name, _)| name.clone()).collect();
		assert!(names.contains(&"crate1".to_string()));
		assert!(names.contains(&"crate2".to_string()));

		// Check paths
		let paths: Vec<PathBuf> = result.iter().map(|(_, path)| path.clone()).collect();
		assert!(paths.contains(&crate1_path));
		assert!(paths.contains(&crate2_path));
	}

	#[test]
	fn get_workspace_project_names_with_glob_patterns_works() {
		let test_builder = TestBuilder::default().add_workspace().add_workspace_cargo_toml(
			r#"[workspace]
members = ["crates/*"]

[workspace.package]
name = "test-workspace"
"#,
		);

		let binding = test_builder.workspace.expect("Workspace should exist");
		let workspace_path = binding.path();

		// Create crates directory
		let crates_dir = workspace_path.join("crates");
		std::fs::create_dir(&crates_dir).expect("Should create crates directory");

		// Create member crates using glob pattern
		let crate1_path = crates_dir.join("crate1");
		std::fs::create_dir(&crate1_path).expect("Should create crate1 directory");
		write(
			crate1_path.join("Cargo.toml"),
			r#"[package]
name = "crate1"
version = "0.1.0"
"#,
		)
		.expect("Should write crate1 Cargo.toml");

		let crate2_path = crates_dir.join("crate2");
		std::fs::create_dir(&crate2_path).expect("Should create crate2 directory");
		write(
			crate2_path.join("Cargo.toml"),
			r#"[package]
name = "crate2"
version = "0.1.0"
"#,
		)
		.expect("Should write crate2 Cargo.toml");

		let result = get_workspace_project_names(workspace_path).expect("Should succeed");
		assert_eq!(result.len(), 2);

		// Check that both crates are found
		let names: Vec<String> = result.iter().map(|(name, _)| name.clone()).collect();
		assert!(names.contains(&"crate1".to_string()));
		assert!(names.contains(&"crate2".to_string()));
	}

	#[test]
	fn get_workspace_project_names_fails_for_non_workspace() {
		let test_builder = TestBuilder::default().add_workspace().add_workspace_cargo_toml(
			r#"[package]
name = "not-a-workspace"
version = "0.1.0"
"#,
		);

		let binding = test_builder.workspace.expect("Workspace should exist");
		let workspace_path = binding.path();

		let result = get_workspace_project_names(workspace_path);
		assert!(result.is_err());
		assert!(matches!(result.unwrap_err(), Error::Config(_)));
	}

	#[test]
	fn get_workspace_project_names_returns_empty_for_no_members() {
		let test_builder = TestBuilder::default().add_workspace().add_workspace_cargo_toml(
			r#"[workspace]
members = []

[workspace.package]
name = "test-workspace"
"#,
		);

		let binding = test_builder.workspace.expect("Workspace should exist");
		let workspace_path = binding.path();

		let result = get_workspace_project_names(workspace_path).expect("Should succeed");
		assert_eq!(result.len(), 0);
	}
}