diff --git a/cli/src/commands/apply_manual.rs b/cli/src/commands/apply_manual.rs deleted file mode 100644 index b8e4383..0000000 --- a/cli/src/commands/apply_manual.rs +++ /dev/null @@ -1,12 +0,0 @@ -use anyhow::Result; -use clap::Parser; - -#[derive(Debug, Parser)] -pub struct ApplyManualArgs { - #[arg(long)] - from_git: String, -} - -pub fn apply_manual_command(_args: &ApplyManualArgs) -> Result<()> { - Ok(()) -} diff --git a/cli/src/commands/compare.rs b/cli/src/commands/compare.rs deleted file mode 100644 index 2049bcb..0000000 --- a/cli/src/commands/compare.rs +++ /dev/null @@ -1,15 +0,0 @@ -use std::path::PathBuf; - -use anyhow::Result; -use clap::Parser; - -#[derive(Debug, Parser)] -pub struct CompareArgs { - first_file: PathBuf, - second_file: PathBuf, -} - -pub fn compare_command(args: &CompareArgs) -> Result<()> { - println!("{:#?} {:#?}", args.first_file, args.second_file); - Ok(()) -} diff --git a/cli/src/commands/init.rs b/cli/src/commands/init.rs index 096f3d8..83838b0 100644 --- a/cli/src/commands/init.rs +++ b/cli/src/commands/init.rs @@ -46,7 +46,7 @@ pub struct NameDependenciesArgs { } -pub fn init_command(args: &InitArgs) -> Result<()> { +pub fn _init_command(args: &InitArgs) -> Result<()> { println!("Planning project onboarding"); println!(" > Generating dependency tree"); diff --git a/cli/src/commands/mirror_dependencies.rs b/cli/src/commands/mirror_dependencies.rs deleted file mode 100644 index 6f4bb34..0000000 --- a/cli/src/commands/mirror_dependencies.rs +++ /dev/null @@ -1,14 +0,0 @@ -use std::path::PathBuf; - -use anyhow::Result; -use clap::Parser; - -#[derive(Debug, Parser)] -pub struct MirrorDependenciesArgs { - dependencies_file: PathBuf, -} - -pub fn mirror_dependencies_command(args: &MirrorDependenciesArgs) -> Result<()> { - println!("{:#?}", args.dependencies_file); - Ok(()) -} diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs index a321642..211a2ea 100644 --- a/cli/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -1,9 +1,5 @@ -pub mod compare; pub mod dependencies; -pub mod mirror_dependencies; pub mod init; -pub mod apply_manual; -pub mod onboard; -pub mod replicate; +pub mod replication; diff --git a/cli/src/commands/onboard.rs b/cli/src/commands/onboard.rs deleted file mode 100644 index a730096..0000000 --- a/cli/src/commands/onboard.rs +++ /dev/null @@ -1,9 +0,0 @@ -use anyhow::Result; -use clap::Parser; - -#[derive(Debug, Parser)] -pub struct OnboardArgs; - -pub fn onboard_command(_args: &OnboardArgs) -> Result<()> { - Ok(()) -} diff --git a/cli/src/commands/replication/apply.rs b/cli/src/commands/replication/apply.rs new file mode 100644 index 0000000..0ad092a --- /dev/null +++ b/cli/src/commands/replication/apply.rs @@ -0,0 +1,43 @@ +use std::{fs::create_dir_all, path::PathBuf}; + +use anyhow::Result; +use clap::Parser; +use source_wand_common::{project_manipulator::{local_project_manipulator::LocalProjectManipulator, project_manipulator::ProjectManipulator}, utils::read_yaml_file::read_yaml_file}; +use source_wand_replication::model::{package_destination::PackageDestination, package_origin::PackageOrigin, replication_plan::ReplicationPlan}; + +#[derive(Debug, Parser)] +pub struct ReplicationApplyArgs; + +pub fn replicate_apply_command(_args: &ReplicationApplyArgs) -> Result<()> { + let replication_plan: ReplicationPlan = read_yaml_file("source-wand/replication-plan.yaml")?; + + for package in replication_plan.packages { + if let PackageOrigin::GoCache(origin) = package.origin { + let PackageDestination::Git(destination) = package.destination; + let dependency_directory: PathBuf = PathBuf::from(format!("./source-wand/dependencies/{}/{}", origin.name, origin.version)); + create_dir_all(&dependency_directory)?; + + let sh: LocalProjectManipulator = LocalProjectManipulator::new(dependency_directory, true); + + println!("Fetching {} ({}) from the local Go Cache", origin.name, origin.version); + sh.run_shell(format!("cp -r {}/* .", origin.path))?; + + let ls_remote: Result = sh.run_shell(format!("git ls-remote --exit-code --heads {} {}", destination.git, destination.reference)); + + if ls_remote.is_ok() { + println!("{} ({}) already exists on remote, skipping", origin.name, origin.version); + continue; + } + + println!("Pushing {} ({}) source code to remote repository", origin.name, origin.version); + sh.run_shell("git init".to_string())?; + sh.run_shell(format!("git remote add origin {}", destination.git))?; + sh.run_shell(format!("git checkout --orphan {}", destination.reference))?; + sh.run_shell("git add .".to_string())?; + sh.run_shell("git commit -m 'Replicate source code'".to_string())?; + sh.run_shell(format!("git push -u origin {}", destination.reference))?; + } + } + + Ok(()) +} diff --git a/cli/src/commands/replication/init.rs b/cli/src/commands/replication/init.rs new file mode 100644 index 0000000..565f4c3 --- /dev/null +++ b/cli/src/commands/replication/init.rs @@ -0,0 +1,30 @@ +use std::{env, path::PathBuf}; + +use anyhow::Result; +use clap::Parser; +use source_wand_common::utils::write_yaml_file::write_yaml_file; +use source_wand_replication::model::{hooks::Hooks, package_destination_git::PackageDestinationGit, package_origin_git::PackageOriginGit, replication_manifest::ReplicationManifest}; + +#[derive(Debug, Parser)] +pub struct ReplicationInitArgs; + +pub fn replicate_init_command(_args: &ReplicationInitArgs) -> Result<()> { + let working_directory: PathBuf = env::current_dir()?; + let project_name = working_directory + .file_name() + .and_then(|os_string| os_string.to_str()) + .ok_or_else(|| anyhow::anyhow!("unknown"))? + .to_string(); + + let replication_manifest: ReplicationManifest = ReplicationManifest::new( + project_name, + Some(Hooks { before_all: None, before_each: None, after_each: None, after_all: None }), + PackageOriginGit::new("".to_string(), "".to_string()), + PackageDestinationGit::new("".to_string(), "".to_string()), + ); + + write_yaml_file(&replication_manifest, "replication.yaml")?; + + println!("Replication project initialized."); + Ok(()) +} diff --git a/cli/src/commands/replication/mod.rs b/cli/src/commands/replication/mod.rs new file mode 100644 index 0000000..2dcb149 --- /dev/null +++ b/cli/src/commands/replication/mod.rs @@ -0,0 +1,34 @@ +use anyhow::Result; +use clap::{command, Parser, Subcommand}; + +use crate::commands::replication::{apply::{replicate_apply_command, ReplicationApplyArgs}, init::{replicate_init_command, ReplicationInitArgs}, plan::{replicate_plan_command, ReplicationPlanArgs}}; + +pub mod init; +pub mod plan; +pub mod apply; + +#[derive(Debug, Parser)] +pub struct ReplicationArgs { + #[command(subcommand)] + pub command: ReplicationCommand, +} + +#[derive(Debug, Subcommand)] +pub enum ReplicationCommand { + #[command(about = "Initialize a new deep replication project")] + Init(ReplicationInitArgs), + + #[command(about = "Plan a deep replication and validate the replication is possible")] + Plan(ReplicationPlanArgs), + + #[command(about = "Apply the deep replication plan")] + Apply(ReplicationApplyArgs), +} + +pub fn replicate_command(args: &ReplicationArgs) -> Result<()> { + match &args.command { + ReplicationCommand::Init(args) => replicate_init_command(args), + ReplicationCommand::Plan(args) => replicate_plan_command(args), + ReplicationCommand::Apply(args) => replicate_apply_command(args), + } +} diff --git a/cli/src/commands/replication/plan.rs b/cli/src/commands/replication/plan.rs new file mode 100644 index 0000000..aaba286 --- /dev/null +++ b/cli/src/commands/replication/plan.rs @@ -0,0 +1,120 @@ +use std::{fs::create_dir_all, path::PathBuf}; + +use anyhow::Result; +use clap::Parser; +use serde_json::Value; +use source_wand_common::{project_manipulator::{local_project_manipulator::LocalProjectManipulator, project_manipulator::ProjectManipulator}, utils::{read_yaml_file::read_yaml_file, write_yaml_file::write_yaml_file}}; +use source_wand_replication::model::{package::Package, package_destination::PackageDestination, package_destination_git::PackageDestinationGit, package_origin::PackageOrigin, package_origin_go_cache::PackageOriginGoCache, replication_manifest::ReplicationManifest, replication_plan::ReplicationPlan}; + +#[derive(Debug, Parser)] +pub struct ReplicationPlanArgs; + +pub fn replicate_plan_command(_args: &ReplicationPlanArgs) -> Result<()> { + let replication_manifest: ReplicationManifest = read_yaml_file("replication.yaml")?; + + match replication_manifest.origin { + PackageOrigin::Git(origin) => { + let top_level_directory: PathBuf = PathBuf::from("./source-wand/top-level/repository"); + create_dir_all(&top_level_directory)?; + + let top_level: LocalProjectManipulator = LocalProjectManipulator::new(top_level_directory, true); + + top_level.run_shell(format!("git clone {} .", origin.git))?; + top_level.run_shell(format!("git checkout {}", origin.reference))?; + + top_level.run_shell("go mod download all".to_string())?; + + let mut packages: Vec = Vec::new(); + + let build_dependencies: serde_json::Value = serde_json::from_str(top_level.run_shell("go list -json -m all | jq -s".to_string())?.as_str())?; + if let Value::Array(build_dependencies) = build_dependencies { + for build_dependency in build_dependencies { + if let Value::Object(build_dependency) = build_dependency { + let name: String = build_dependency.get("Path").unwrap().as_str().unwrap_or_default().replace("/", "-"); + let version: String = build_dependency.get("Version").unwrap_or(&Value::String(origin.reference.split('/').last().unwrap_or_default().to_string())).as_str().unwrap_or_default().to_string(); + let cache_path: String = build_dependency.get("Dir").unwrap_or(&Value::String(String::new())).as_str().unwrap_or_default().to_string(); + + let (version_major, version_minor, version_patch, version_suffix, version_retrocompatible) = { + let mut major: String = String::new(); + let mut minor: String = String::new(); + let mut patch: String = String::new(); + let mut suffix: String = String::new(); + + if version.starts_with('v') { + let parts: Vec<&str> = version.trim_start_matches('v').split('-').collect(); + let semantic_version_parts: Vec<&str> = parts[0].split('.').collect(); + + if semantic_version_parts.len() > 0 { + major = semantic_version_parts[0].to_string(); + } + if semantic_version_parts.len() > 1 { + minor = semantic_version_parts[1].to_string(); + } + if semantic_version_parts.len() > 2 { + patch = semantic_version_parts[2].to_string(); + } + + if parts.len() > 1 { + suffix = format!("-{}", parts[1..].join("-")); + } + } + + let retrocompatible: String = + if suffix.is_empty() { + if major == "0".to_string() { + format!("{}.{}.{}", major.clone(), minor, patch) + } + else { + major.clone() + } + } + else { + format!("{}.{}.{}-{}", major, minor, patch, suffix) + }; + + (major, minor, patch, suffix, retrocompatible) + }; + + let PackageDestination::Git(package_destination) = &replication_manifest.destination_template; + let package_destination_url: String = package_destination.git + .replace("$NAME", &name) + .replace("$VERSION_MAJOR", &version_major) + .replace("$VERSION_MINOR", &version_minor) + .replace("$VERSION_PATCH", &version_patch) + .replace("$VERSION_SUFFIX", &version_suffix) + .replace("$VERSION_RETROCOMPATIBLE", &version_retrocompatible) + .replace("$VERSION", &version); + let package_destination_reference: String = package_destination.reference + .replace("$NAME", &name) + .replace("$VERSION_MAJOR", &version_major) + .replace("$VERSION_MINOR", &version_minor) + .replace("$VERSION_PATCH", &version_patch) + .replace("$VERSION_SUFFIX", &version_suffix) + .replace("$VERSION_RETROCOMPATIBLE", &version_retrocompatible) + .replace("$VERSION", &version); + + let package: Package = Package::new( + 0, + PackageOriginGoCache::new(name, version, cache_path), + PackageDestinationGit::new( + package_destination_url, + package_destination_reference, + ), + Vec::new(), + ); + + packages.push(package); + } + } + } + + let replication_plan: ReplicationPlan = ReplicationPlan::new(replication_manifest.project, replication_manifest.hooks, packages); + write_yaml_file(&replication_plan, "./source-wand/replication-plan.yaml")?; + + top_level.cleanup(); + }, + PackageOrigin::GoCache(_origin) => {}, + } + + Ok(()) +} diff --git a/cli/src/main.rs b/cli/src/main.rs index 2ad554f..1e430a7 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,33 +1,13 @@ use anyhow::Result; use clap::{Parser, Subcommand}; use commands::{ - apply_manual::{ - apply_manual_command, - ApplyManualArgs - }, - compare::{ - compare_command, - CompareArgs - }, dependencies::{ dependencies_command, DependenciesArgs - }, - init::{ - init_command, - InitArgs - }, - mirror_dependencies::{ - mirror_dependencies_command, - MirrorDependenciesArgs - }, - onboard::{ - onboard_command, - OnboardArgs } }; -use crate::commands::replicate::{replicate_command, ReplicateArgs}; +use crate::commands::replication::{replicate_command, ReplicationArgs}; mod commands; @@ -42,33 +22,13 @@ enum Command { #[command(about = "Find the dependency tree of a project.")] Dependencies(DependenciesArgs), - #[command(about = "Compare dependency lists")] - Compare(CompareArgs), - - #[command(about = "Mirror dependencies")] - MirrorDependencies(MirrorDependenciesArgs), - - #[command(about = "Initialize the onboarding of a project")] - Init(InitArgs), - - #[command(about = "Try to add your manual configurations to automated onboarding")] - ApplyManual(ApplyManualArgs), - - #[command(about = "Onboard a project and its dependencies")] - Onboard(OnboardArgs), - #[command(about = "Replicate a project along with its dependencies")] - Replicate(ReplicateArgs) + Replication(ReplicationArgs) } fn main() -> Result<()> { match Cli::parse().command { Command::Dependencies(args) => dependencies_command(&args), - Command::Compare(args) => compare_command(&args), - Command::MirrorDependencies(args) => mirror_dependencies_command(&args), - Command::Init(args) => init_command(&args), - Command::ApplyManual(args) => apply_manual_command(&args), - Command::Onboard(args) => onboard_command(&args), - Command::Replicate(args) => replicate_command(&args), + Command::Replication(args) => replicate_command(&args), } } diff --git a/common/src/project_manipulator/local_project_manipulator.rs b/common/src/project_manipulator/local_project_manipulator.rs index 740660a..c1aca1a 100644 --- a/common/src/project_manipulator/local_project_manipulator.rs +++ b/common/src/project_manipulator/local_project_manipulator.rs @@ -36,7 +36,11 @@ impl ProjectManipulator for LocalProjectManipulator { fn try_run_shell(&self, command: String, retries: u32) -> Result { self.to_any().try_run_shell(command, retries) } - + + fn get_working_directory(&self) -> PathBuf { + self.project_root.clone() + } + fn cleanup(&self) { if self.should_cleanup { let _ = self.run_shell( diff --git a/common/src/project_manipulator/lxd_project_manipulator.rs b/common/src/project_manipulator/lxd_project_manipulator.rs index 702fd09..333f0eb 100644 --- a/common/src/project_manipulator/lxd_project_manipulator.rs +++ b/common/src/project_manipulator/lxd_project_manipulator.rs @@ -71,6 +71,10 @@ impl ProjectManipulator for LxdProjectManipulator { self.to_any().try_run_shell(command, retries) } + fn get_working_directory(&self) -> PathBuf { + self.project_root.clone() + } + fn cleanup(&self) { let _ = self.run_shell( format!( diff --git a/common/src/project_manipulator/project_manipulator.rs b/common/src/project_manipulator/project_manipulator.rs index 7ea3aa3..322dcee 100644 --- a/common/src/project_manipulator/project_manipulator.rs +++ b/common/src/project_manipulator/project_manipulator.rs @@ -1,8 +1,12 @@ +use std::path::PathBuf; + use anyhow::{Error, Result}; -use crate::dependency_ensurer::required_dependency::{ - AnyRequiredDependency, - RequiredDependency +use crate::{ + dependency_ensurer::required_dependency::{ + AnyRequiredDependency, + RequiredDependency + } }; use super::{ @@ -13,6 +17,7 @@ use super::{ pub trait ProjectManipulator { fn run_shell(&self, command: String) -> Result; fn try_run_shell(&self, command: String, retries: u32) -> Result; + fn get_working_directory(&self) -> PathBuf; fn cleanup(&self); } @@ -61,7 +66,18 @@ impl ProjectManipulator for AnyProjectManipulator { Err(error) } - + + fn get_working_directory(&self) -> PathBuf { + match self { + AnyProjectManipulator::LocalManipulator(project_manipulator) => { + project_manipulator.get_working_directory() + }, + AnyProjectManipulator::LxdManipulator(project_manipulator) => { + project_manipulator.get_working_directory() + }, + } + } + fn cleanup(&self) { match self { AnyProjectManipulator::LocalManipulator(project_manipulator) => { diff --git a/dependency-analysis/src/build_systems/build_system_identity.rs b/dependency-analysis/src/build_systems/build_system_identity.rs index 8600dc4..f4e9eed 100644 --- a/dependency-analysis/src/build_systems/build_system_identity.rs +++ b/dependency-analysis/src/build_systems/build_system_identity.rs @@ -12,6 +12,7 @@ pub enum BuildSystemIdentity { PythonPip, JavaMaven, Go, + Unknown, } impl BuildSystemIdentity { @@ -38,6 +39,7 @@ impl BuildSystemIdentity { GoDependency::to_any(), ] }, + BuildSystemIdentity::Unknown => { vec![] }, } } } diff --git a/dependency-analysis/src/build_systems/identifier.rs b/dependency-analysis/src/build_systems/identifier.rs index a1b3538..64795cb 100644 --- a/dependency-analysis/src/build_systems/identifier.rs +++ b/dependency-analysis/src/build_systems/identifier.rs @@ -1,4 +1,4 @@ -use anyhow::{Error, Result}; +use anyhow::{Ok, Result}; use source_wand_common::project_manipulator::project_manipulator::{AnyProjectManipulator, ProjectManipulator}; use super::build_system_identity::BuildSystemIdentity; @@ -20,5 +20,7 @@ pub fn identify_build_system(project_manipulator: &AnyProjectManipulator) -> Res return Ok(BuildSystemIdentity::Go) } - Err(Error::msg("Unable to identify the build system of the project.".to_string())) + return Ok(BuildSystemIdentity::Unknown) + + // Err(Error::msg("Unable to identify the build system of the project.".to_string())) } diff --git a/dependency-analysis/src/dependency_tree_generators/cdxgen_dependency_tree_generator.rs b/dependency-analysis/src/dependency_tree_generators/cdxgen_dependency_tree_generator.rs new file mode 100644 index 0000000..ae06668 --- /dev/null +++ b/dependency-analysis/src/dependency_tree_generators/cdxgen_dependency_tree_generator.rs @@ -0,0 +1,187 @@ +use std::{ + collections::HashMap, fs, path::PathBuf, +}; + +use anyhow::{Error, Result}; +use serde::Deserialize; +use source_wand_common::{ + project::Project, project_manipulator::project_manipulator::ProjectManipulator, +}; + +use crate::dependency_tree_node::DependencyTreeNode; + +#[derive(Debug, Deserialize)] +struct Bom { + metadata: BomMetadata, + components: Vec, + dependencies: Vec, +} + +#[derive(Debug, Deserialize)] +struct BomMetadata { + component: Component, +} + +#[derive(Debug, Deserialize)] +struct Property { + name: String, + value: Option, +} + +#[derive(Debug, Deserialize)] +struct Component { + #[serde(rename = "bom-ref")] + bom_ref: String, + name: String, + version: Option, + #[serde(default)] + properties: Vec, +} + +#[derive(Debug, Deserialize)] +struct BomDependency { + #[serde(rename = "ref")] + ref_: String, + #[serde(rename = "dependsOn")] + depends_on: Vec, +} + +pub fn generate_cdxgen_dependency_tree( + project_manipulator: &dyn ProjectManipulator, + _language: Option<&str>, +) -> Result { + project_manipulator.run_shell( + format!( + "cdxgen -o bom.source-wand.json --output-format json", + ) + )?; + + let bom_path: PathBuf = project_manipulator.get_working_directory().join("bom.source-wand.json"); + + let bom_raw = fs::read_to_string(&bom_path) + .map_err(|e| Error::msg(format!("Failed to read bom.json: {}", e)))?; + + fs::remove_file(&bom_path) + .map_err(|e| Error::msg(format!("Failed to remove bom.json: {}", e)))?; + + let bom: Bom = serde_json::from_str(&bom_raw) + .map_err( + |e| { + Error::msg( + format!( + "Failed to parse BOM JSON: {}\n\nHere is the BOM in question:\n{}", + e, + serde_json::to_string_pretty( + &serde_json::from_str::(&bom_raw.as_str()).unwrap() + ).unwrap() + ) + ) + } + )?; + + let mut component_map: HashMap = HashMap::new(); + + let root_project: Project = Project::new( + bom.metadata.component.name, + bom.metadata.component.version.unwrap_or_else(|| "Not found".to_string()), + String::new(), + String::new(), + None, + None, + ); + component_map.insert(bom.metadata.component.bom_ref.clone(), root_project); + + for component in bom.components { + let mut group_id = Some(String::new()); + let mut artifact_id = Some(String::new()); + + for prop in &component.properties { + if prop.name == "group_id" { + group_id = prop.value.clone(); + } else if prop.name == "artifact_id" { + artifact_id = prop.value.clone(); + } + } + + let project = Project::new( + component.name, + component.version.unwrap_or_else(|| "Not found".to_string()), + group_id.unwrap_or_default(), + artifact_id.unwrap_or_default(), + None, + None, + ); + component_map.insert(component.bom_ref, project); + } + + let mut dependency_map: HashMap> = HashMap::new(); + + for dep in bom.dependencies { + dependency_map.insert(dep.ref_, dep.depends_on); + } + + let root_bom_ref = bom.metadata.component.bom_ref; + let root_node = build_node_iterative(&root_bom_ref, &component_map, &dependency_map)?; + + Ok(*root_node) +} + +struct StackFrame<'a> { + component_ref: String, + child_refs_iter: std::slice::Iter<'a, String>, + children: Vec>, +} + +fn build_node_iterative( + root_ref: &str, + component_map: &HashMap, + dependency_map: &HashMap>, +) -> Result> { + let mut stack: Vec = Vec::new(); + + let root_children = dependency_map.get(root_ref) + .map(|v| v.as_slice()) + .unwrap_or(&[]); + + stack.push(StackFrame { + component_ref: root_ref.to_owned(), + child_refs_iter: root_children.iter(), + children: Vec::new(), + }); + + let mut result: Option> = None; + + while let Some(top) = stack.last_mut() { + if let Some(next_child_ref) = top.child_refs_iter.next() { + // For each child, push a new frame on the stack + let child_children = dependency_map.get(next_child_ref.as_str()) + .map(|v| v.as_slice()) + .unwrap_or(&[]); + + stack.push(StackFrame { + component_ref: next_child_ref.to_owned(), + child_refs_iter: child_children.iter(), + children: Vec::new(), + }); + } else { + // No more children remain, can build node from children and pop + let frame = stack.pop().unwrap(); + + let project = component_map.get(&frame.component_ref) + .ok_or_else(|| Error::msg(format!("Component with ref '{}' not found in map", &frame.component_ref)))? + .clone(); + + let node = Box::new(DependencyTreeNode::new(project, frame.children)); + + if let Some(parent) = stack.last_mut() { + // Add this node to parent's children vec + parent.children.push(node); + } else { + // No parent, this is the root node + result = Some(node); + } + } + } + + result.ok_or_else(|| Error::msg("Failed to build dependency tree")) +} diff --git a/dependency-analysis/src/dependency_tree_generators/cdxgen_go_dependency_tree_generator.rs b/dependency-analysis/src/dependency_tree_generators/cdxgen_go_dependency_tree_generator.rs new file mode 100644 index 0000000..57d6944 --- /dev/null +++ b/dependency-analysis/src/dependency_tree_generators/cdxgen_go_dependency_tree_generator.rs @@ -0,0 +1,147 @@ +use std::{ + collections::HashMap, fs, path::PathBuf +}; + +use anyhow::{Error, Result}; +use serde::Deserialize; +use source_wand_common::{ + project::Project, project_manipulator::project_manipulator::ProjectManipulator +}; + +use crate::dependency_tree_node::DependencyTreeNode; + +#[derive(Debug, Deserialize)] +struct Bom { + metadata: BomMetadata, + components: Vec, + dependencies: Vec, +} + +#[derive(Debug, Deserialize)] +struct BomMetadata { + component: Component, +} + +#[derive(Debug, Deserialize)] +struct Property { + name: String, + value: Option, +} + +#[derive(Debug, Deserialize)] +struct Component { + #[serde(rename = "bom-ref")] + bom_ref: String, + name: String, + version: String, + #[serde(default)] + properties: Vec, +} + +#[derive(Debug, Deserialize)] +struct BomDependency { + #[serde(rename = "ref")] + ref_: String, + #[serde(rename = "dependsOn")] + depends_on: Vec, +} + +pub fn generate_cdxgen_go_dependency_tree( + project_manipulator: &dyn ProjectManipulator, +) -> Result { + project_manipulator.run_shell("cdxgen -t go -o bom.source-wand.json --output-format json".to_string())?; + + let bom_path: PathBuf = project_manipulator.get_working_directory().join("bom.source-wand.json"); + + let bom_raw = fs::read_to_string(&bom_path) + .map_err(|e| Error::msg(format!("Failed to read bom.json: {}", e)))?; + + fs::remove_file(&bom_path) + .map_err(|e| Error::msg(format!("Failed to remove bom.json: {}", e)))?; + + let bom: Bom = serde_json::from_str(&bom_raw) + .map_err( + |e| { + Error::msg( + format!( + "Failed to parse BOM JSON: {}\n\nHere is the BOM in question:\n{}", + e, + serde_json::to_string_pretty( + &serde_json::from_str::(&bom_raw.as_str()).unwrap() + ).unwrap() + ) + ) + } + )?; + + let mut component_map: HashMap = HashMap::new(); + + let root_project: Project = Project::new( + bom.metadata.component.name, + bom.metadata.component.version, + String::new(), + String::new(), + None, + None, + ); + component_map.insert(bom.metadata.component.bom_ref.clone(), root_project); + + for component in bom.components { + let mut group_id = Some(String::new()); + let mut artifact_id = Some(String::new()); + + for prop in &component.properties { + if prop.name == "group_id" { + group_id = prop.value.clone(); + } else if prop.name == "artifact_id" { + artifact_id = prop.value.clone(); + } + } + + let project = Project::new( + component.name, + component.version, + group_id.unwrap_or_default(), + artifact_id.unwrap_or_default(), + None, + None, + ); + component_map.insert(component.bom_ref, project); + } + + let mut dependency_map: HashMap> = HashMap::new(); + + for dep in bom.dependencies { + dependency_map.insert(dep.ref_, dep.depends_on); + } + + let root_bom_ref = bom.metadata.component.bom_ref; + let root_node = build_node(&root_bom_ref, &component_map, &dependency_map)?; + + Ok(*root_node) +} + +fn build_node( + component_ref: &str, + component_map: &HashMap, + dependency_map: &HashMap>, +) -> Result> { + let project = component_map + .get(component_ref) + .ok_or_else(|| Error::msg(format!("Component with ref '{}' not found in map", component_ref)))? + .clone(); + + let binding = Vec::new(); + let direct_dependencies_refs = dependency_map + .get(component_ref) + .unwrap_or(&binding); + + let mut child_dependencies: Vec> = Vec::new(); + + for dep_ref in direct_dependencies_refs { + let child_node = build_node(dep_ref, component_map, dependency_map)?; + child_dependencies.push(child_node); + } + + Ok(Box::new(DependencyTreeNode::new(project, child_dependencies))) +} diff --git a/dependency-analysis/src/dependency_tree_generators/cdxgen_java_dependency_tree_generator.rs b/dependency-analysis/src/dependency_tree_generators/cdxgen_java_dependency_tree_generator.rs new file mode 100644 index 0000000..37d30d2 --- /dev/null +++ b/dependency-analysis/src/dependency_tree_generators/cdxgen_java_dependency_tree_generator.rs @@ -0,0 +1,147 @@ +use std::{ + collections::HashMap, fs, path::PathBuf +}; + +use anyhow::{Error, Result}; +use serde::Deserialize; +use source_wand_common::{ + project::Project, project_manipulator::project_manipulator::ProjectManipulator +}; + +use crate::dependency_tree_node::DependencyTreeNode; + +#[derive(Debug, Deserialize)] +struct Bom { + metadata: BomMetadata, + components: Vec, + dependencies: Vec, +} + +#[derive(Debug, Deserialize)] +struct BomMetadata { + component: Component, +} + +#[derive(Debug, Deserialize)] +struct Property { + name: String, + value: Option, +} + +#[derive(Debug, Deserialize)] +struct Component { + #[serde(rename = "bom-ref")] + bom_ref: String, + name: String, + version: String, + #[serde(default)] + properties: Vec, +} + +#[derive(Debug, Deserialize)] +struct BomDependency { + #[serde(rename = "ref")] + ref_: String, + #[serde(rename = "dependsOn")] + depends_on: Vec, +} + +pub fn generate_cdxgen_java_dependency_tree( + project_manipulator: &dyn ProjectManipulator, +) -> Result { + project_manipulator.run_shell("cdxgen -t java -o bom.source-wand.json --output-format json".to_string())?; + + let bom_path: PathBuf = project_manipulator.get_working_directory().join("bom.source-wand.json"); + + let bom_raw = fs::read_to_string(&bom_path) + .map_err(|e| Error::msg(format!("Failed to read bom.json: {}", e)))?; + + fs::remove_file(&bom_path) + .map_err(|e| Error::msg(format!("Failed to remove bom.json: {}", e)))?; + + let bom: Bom = serde_json::from_str(&bom_raw) + .map_err( + |e| { + Error::msg( + format!( + "Failed to parse BOM JSON: {}\n\nHere is the BOM in question:\n{}", + e, + serde_json::to_string_pretty( + &serde_json::from_str::(&bom_raw.as_str()).unwrap() + ).unwrap() + ) + ) + } + )?; + + let mut component_map: HashMap = HashMap::new(); + + let root_project: Project = Project::new( + bom.metadata.component.name, + bom.metadata.component.version, + String::new(), + String::new(), + None, + None, + ); + component_map.insert(bom.metadata.component.bom_ref.clone(), root_project); + + for component in bom.components { + let mut group_id = Some(String::new()); + let mut artifact_id = Some(String::new()); + + for prop in &component.properties { + if prop.name == "group_id" { + group_id = prop.value.clone(); + } else if prop.name == "artifact_id" { + artifact_id = prop.value.clone(); + } + } + + let project = Project::new( + component.name, + component.version, + group_id.unwrap_or_default(), + artifact_id.unwrap_or_default(), + None, + None, + ); + component_map.insert(component.bom_ref, project); + } + + let mut dependency_map: HashMap> = HashMap::new(); + + for dep in bom.dependencies { + dependency_map.insert(dep.ref_, dep.depends_on); + } + + let root_bom_ref = bom.metadata.component.bom_ref; + let root_node = build_node(&root_bom_ref, &component_map, &dependency_map)?; + + Ok(*root_node) +} + +fn build_node( + component_ref: &str, + component_map: &HashMap, + dependency_map: &HashMap>, +) -> Result> { + let project = component_map + .get(component_ref) + .ok_or_else(|| Error::msg(format!("Component with ref '{}' not found in map", component_ref)))? + .clone(); + + let binding = Vec::new(); + let direct_dependencies_refs = dependency_map + .get(component_ref) + .unwrap_or(&binding); + + let mut child_dependencies: Vec> = Vec::new(); + + for dep_ref in direct_dependencies_refs { + let child_node = build_node(dep_ref, component_map, dependency_map)?; + child_dependencies.push(child_node); + } + + Ok(Box::new(DependencyTreeNode::new(project, child_dependencies))) +} diff --git a/dependency-analysis/src/dependency_tree_generators/cdxgen_python_dependency_tree_generator.rs b/dependency-analysis/src/dependency_tree_generators/cdxgen_python_dependency_tree_generator.rs new file mode 100644 index 0000000..6c54af5 --- /dev/null +++ b/dependency-analysis/src/dependency_tree_generators/cdxgen_python_dependency_tree_generator.rs @@ -0,0 +1,135 @@ +use std::{ + collections::HashMap, fs, path::PathBuf +}; + +use anyhow::{Error, Result}; +use serde::Deserialize; +use source_wand_common::{ + project::Project, project_manipulator::project_manipulator::ProjectManipulator +}; + +use crate::dependency_tree_node::DependencyTreeNode; + +#[derive(Debug, Deserialize)] +struct Bom { + metadata: BomMetadata, + components: Vec, + dependencies: Vec, +} + +#[derive(Debug, Deserialize)] +struct BomMetadata { + component: Component, +} + +#[derive(Debug, Deserialize)] +struct Property { + name: String, + value: Option, +} + +#[derive(Debug, Deserialize)] +struct Component { + #[serde(rename = "bom-ref")] + bom_ref: String, + name: String, + version: String, + #[serde(default)] + properties: Vec, +} + +#[derive(Debug, Deserialize)] +struct BomDependency { + #[serde(rename = "ref")] + ref_: String, + #[serde(rename = "dependsOn")] + depends_on: Vec, +} + +pub fn generate_cdxgen_python_dependency_tree( + project_manipulator: &dyn ProjectManipulator, +) -> Result { + project_manipulator.run_shell("cdxgen -t python -o bom.source-wand.json --output-format json --feature-flags safe-pip-install".to_string())?; + + let bom_path: PathBuf = project_manipulator.get_working_directory().join("bom.source-wand.json"); + + let bom_raw = fs::read_to_string(&bom_path) + .map_err(|e| Error::msg(format!("Failed to read bom.json: {}", e)))?; + + fs::remove_file(&bom_path) + .map_err(|e| Error::msg(format!("Failed to remove bom.json: {}", e)))?; + + let bom: Bom = serde_json::from_str(&bom_raw) + .map_err(|e| Error::msg(format!("Failed to parse BOM JSON: {}\n\nHere is the BOM in question:\n{}", e, bom_raw)))?; + + let mut component_map: HashMap = HashMap::new(); + + let root_project: Project = Project::new( + bom.metadata.component.name, + bom.metadata.component.version, + String::new(), + String::new(), + None, + None, + ); + component_map.insert(bom.metadata.component.bom_ref.clone(), root_project); + + for component in bom.components { + let mut group_id = Some(String::new()); + let mut artifact_id = Some(String::new()); + + for prop in &component.properties { + if prop.name == "group_id" { + group_id = prop.value.clone(); + } else if prop.name == "artifact_id" { + artifact_id = prop.value.clone(); + } + } + + let project = Project::new( + component.name, + component.version, + group_id.unwrap_or_default(), + artifact_id.unwrap_or_default(), + None, + None, + ); + component_map.insert(component.bom_ref, project); + } + + let mut dependency_map: HashMap> = HashMap::new(); + + for dep in bom.dependencies { + dependency_map.insert(dep.ref_, dep.depends_on); + } + + let root_bom_ref = bom.metadata.component.bom_ref; + let root_node = build_node(&root_bom_ref, &component_map, &dependency_map)?; + + Ok(*root_node) +} + +fn build_node( + component_ref: &str, + component_map: &HashMap, + dependency_map: &HashMap>, +) -> Result> { + let project = component_map + .get(component_ref) + .ok_or_else(|| Error::msg(format!("Component with ref '{}' not found in map", component_ref)))? + .clone(); + + let binding = Vec::new(); + let direct_dependencies_refs = dependency_map + .get(component_ref) + .unwrap_or(&binding); + + let mut child_dependencies: Vec> = Vec::new(); + + for dep_ref in direct_dependencies_refs { + let child_node = build_node(dep_ref, component_map, dependency_map)?; + child_dependencies.push(child_node); + } + + Ok(Box::new(DependencyTreeNode::new(project, child_dependencies))) +} diff --git a/dependency-analysis/src/dependency_tree_generators/cdxgen_rust_dependency_tree_generator.rs b/dependency-analysis/src/dependency_tree_generators/cdxgen_rust_dependency_tree_generator.rs new file mode 100644 index 0000000..20b1aae --- /dev/null +++ b/dependency-analysis/src/dependency_tree_generators/cdxgen_rust_dependency_tree_generator.rs @@ -0,0 +1,147 @@ +use std::{ + collections::HashMap, fs, path::PathBuf +}; + +use anyhow::{Error, Result}; +use serde::Deserialize; +use source_wand_common::{ + project::Project, project_manipulator::project_manipulator::ProjectManipulator +}; + +use crate::dependency_tree_node::DependencyTreeNode; + +#[derive(Debug, Deserialize)] +struct Bom { + metadata: BomMetadata, + components: Vec, + dependencies: Vec, +} + +#[derive(Debug, Deserialize)] +struct BomMetadata { + component: Component, +} + +#[derive(Debug, Deserialize)] +struct Property { + name: String, + value: Option, +} + +#[derive(Debug, Deserialize)] +struct Component { + #[serde(rename = "bom-ref")] + bom_ref: String, + name: String, + version: String, + #[serde(default)] + properties: Vec, +} + +#[derive(Debug, Deserialize)] +struct BomDependency { + #[serde(rename = "ref")] + ref_: String, + #[serde(rename = "dependsOn")] + depends_on: Vec, +} + +pub fn generate_cdxgen_rust_dependency_tree( + project_manipulator: &dyn ProjectManipulator, +) -> Result { + project_manipulator.run_shell("cdxgen -t rust -o bom.source-wand.json --output-format json".to_string())?; + + let bom_path: PathBuf = project_manipulator.get_working_directory().join("bom.source-wand.json"); + + let bom_raw = fs::read_to_string(&bom_path) + .map_err(|e| Error::msg(format!("Failed to read bom.json: {}", e)))?; + + fs::remove_file(&bom_path) + .map_err(|e| Error::msg(format!("Failed to remove bom.json: {}", e)))?; + + let bom: Bom = serde_json::from_str(&bom_raw) + .map_err( + |e| { + Error::msg( + format!( + "Failed to parse BOM JSON: {}\n\nHere is the BOM in question:\n{}", + e, + serde_json::to_string_pretty( + &serde_json::from_str::(&bom_raw.as_str()).unwrap() + ).unwrap() + ) + ) + } + )?; + + let mut component_map: HashMap = HashMap::new(); + + let root_project: Project = Project::new( + bom.metadata.component.name, + bom.metadata.component.version, + String::new(), + String::new(), + None, + None, + ); + component_map.insert(bom.metadata.component.bom_ref.clone(), root_project); + + for component in bom.components { + let mut group_id = Some(String::new()); + let mut artifact_id = Some(String::new()); + + for prop in &component.properties { + if prop.name == "group_id" { + group_id = prop.value.clone(); + } else if prop.name == "artifact_id" { + artifact_id = prop.value.clone(); + } + } + + let project = Project::new( + component.name, + component.version, + group_id.unwrap_or_default(), + artifact_id.unwrap_or_default(), + None, + None, + ); + component_map.insert(component.bom_ref, project); + } + + let mut dependency_map: HashMap> = HashMap::new(); + + for dep in bom.dependencies { + dependency_map.insert(dep.ref_, dep.depends_on); + } + + let root_bom_ref = bom.metadata.component.bom_ref; + let root_node = build_node(&root_bom_ref, &component_map, &dependency_map)?; + + Ok(*root_node) +} + +fn build_node( + component_ref: &str, + component_map: &HashMap, + dependency_map: &HashMap>, +) -> Result> { + let project = component_map + .get(component_ref) + .ok_or_else(|| Error::msg(format!("Component with ref '{}' not found in map", component_ref)))? + .clone(); + + let binding = Vec::new(); + let direct_dependencies_refs = dependency_map + .get(component_ref) + .unwrap_or(&binding); + + let mut child_dependencies: Vec> = Vec::new(); + + for dep_ref in direct_dependencies_refs { + let child_node = build_node(dep_ref, component_map, dependency_map)?; + child_dependencies.push(child_node); + } + + Ok(Box::new(DependencyTreeNode::new(project, child_dependencies))) +} diff --git a/dependency-analysis/src/dependency_tree_generators/mod.rs b/dependency-analysis/src/dependency_tree_generators/mod.rs index c6ebf6a..8249104 100644 --- a/dependency-analysis/src/dependency_tree_generators/mod.rs +++ b/dependency-analysis/src/dependency_tree_generators/mod.rs @@ -1,12 +1,9 @@ use anyhow::Result; -use java_maven_dependency_tree_generator::generate_java_maven_dependency_tree; -use go_dependency_tree_generator::generate_go_dependency_tree; -use python_pip_dependency_tree_generator::generate_python_pip_dependency_tree; use source_wand_common::project_manipulator::project_manipulator::AnyProjectManipulator; -use rust_cargo_dependency_tree_generator::generate_rust_cargo_dependency_tree; use crate::{ build_systems::build_system_identity::BuildSystemIdentity, + dependency_tree_generators::cdxgen_dependency_tree_generator::generate_cdxgen_dependency_tree, dependency_tree_node::DependencyTreeNode }; @@ -15,22 +12,31 @@ pub mod python_pip_dependency_tree_generator; pub mod java_maven_dependency_tree_generator; pub mod go_dependency_tree_generator; +pub mod cdxgen_dependency_tree_generator; +pub mod cdxgen_rust_dependency_tree_generator; +pub mod cdxgen_python_dependency_tree_generator; +pub mod cdxgen_java_dependency_tree_generator; +pub mod cdxgen_go_dependency_tree_generator; + pub fn generate_dependency_tree( build_system: BuildSystemIdentity, project_manipulator: &AnyProjectManipulator ) -> Result { match build_system { BuildSystemIdentity::RustCargo => { - generate_rust_cargo_dependency_tree(project_manipulator) + generate_cdxgen_dependency_tree(project_manipulator, Some("rust")) }, BuildSystemIdentity::PythonPip => { - generate_python_pip_dependency_tree(project_manipulator) + generate_cdxgen_dependency_tree(project_manipulator, Some("python")) }, BuildSystemIdentity::JavaMaven => { - generate_java_maven_dependency_tree(project_manipulator) + generate_cdxgen_dependency_tree(project_manipulator, Some("java")) }, BuildSystemIdentity::Go => { - generate_go_dependency_tree(project_manipulator) + generate_cdxgen_dependency_tree(project_manipulator, Some("go")) }, + BuildSystemIdentity::Unknown => { + generate_cdxgen_dependency_tree(project_manipulator, None) + } } } diff --git a/dependency-analysis/src/lib.rs b/dependency-analysis/src/lib.rs index 47a77b5..149bec4 100644 --- a/dependency-analysis/src/lib.rs +++ b/dependency-analysis/src/lib.rs @@ -64,8 +64,8 @@ pub fn find_dependency_tree(request: DependencyTreeRequest) -> Result = build_system.get_required_dependencies(); - project_manipulator.ensure_dependencies(dependencies)?; + // let dependencies: Vec = build_system.get_required_dependencies(); + // project_manipulator.ensure_dependencies(dependencies)?; let dependency_tree: Result = generate_dependency_tree(build_system, &project_manipulator); project_manipulator.cleanup(); diff --git a/snapcraft.yaml b/snapcraft.yaml index 367c1b3..61d3200 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -1,5 +1,5 @@ name: source-wand -version: 0.0.1 +version: 0.14.0 summary: CLI tool that helps with large scale manipulations of source code description: | `source-wand` is a CLI tool that helps with large scale manipulations of source code. @@ -47,24 +47,34 @@ parts: export CARGO_HOME=$SNAPCRAFT_PART_INSTALL/rust/cargo export RUSTUP_HOME=$SNAPCRAFT_PART_INSTALL/rust/rustup curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path + cdxgen: + plugin: npm + npm-node-version: "20.19.4" + npm-include-node: true + source: https://github.com/CycloneDX/cdxgen + source-type: git source-wand: plugin: rust build-packages: - libssl-dev - pkg-config stage-packages: - - git - - curl - ca-certificates + - curl + - git + - jq source: . - after: [rust-deps] + after: + - go-toolchain + - rust-deps + - cdxgen rust-path: ["cli"] apps: source-wand: command: bin/source-wand environment: - PATH: $SNAP/rust/cargo/bin:$SNAP/go-toolchain/bin:$PATH + PATH: $SNAP/node/bin:$SNAP/rust/cargo/bin:$SNAP/go-toolchain/bin:$PATH GIT_EXEC_PATH: $SNAP/usr/lib/git-core