From fdad8995a41784d934e50c5f9ec12979db1ccead Mon Sep 17 00:00:00 2001 From: daimond113 <72147841+daimond113@users.noreply.github.com> Date: Sun, 14 Jul 2024 15:19:15 +0200 Subject: [PATCH] feat: implement dependency resolver and lune scripts --- .github/workflows/test-and-lint.yaml | 2 +- rustfmt.toml | 1 + src/cli/init.rs | 113 +++++++++++ src/cli/install.rs | 13 ++ src/cli/mod.rs | 27 ++- src/cli/run.rs | 48 +++++ src/lib.rs | 35 ++++ src/lockfile.rs | 56 +++++- src/main.rs | 4 +- src/manifest.rs | 216 +++++++++++++------- src/names.rs | 8 + src/resolver.rs | 284 +++++++++++++++++++++++++++ src/scripts.rs | 80 ++++++++ src/source/mod.rs | 149 ++++++++++---- src/source/pesde.rs | 131 ++++++------ 15 files changed, 994 insertions(+), 173 deletions(-) create mode 100644 rustfmt.toml create mode 100644 src/cli/init.rs create mode 100644 src/cli/install.rs create mode 100644 src/cli/run.rs create mode 100644 src/resolver.rs create mode 100644 src/scripts.rs diff --git a/.github/workflows/test-and-lint.yaml b/.github/workflows/test-and-lint.yaml index 1b20bd7..384bba6 100644 --- a/.github/workflows/test-and-lint.yaml +++ b/.github/workflows/test-and-lint.yaml @@ -18,7 +18,7 @@ jobs: components: rustfmt, clippy - name: Run tests - run: cargo test --all + run: cargo test --all --all-features - name: Check formatting run: cargo fmt --all -- --check diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..d9ba5fd --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +imports_granularity = "Crate" \ No newline at end of file diff --git a/src/cli/init.rs b/src/cli/init.rs new file mode 100644 index 0000000..9485a42 --- /dev/null +++ b/src/cli/init.rs @@ -0,0 +1,113 @@ +use crate::cli::read_config; +use clap::Args; +use colored::Colorize; +use inquire::validator::Validation; +use pesde::{errors::ManifestReadError, names::PackageName, Project, DEFAULT_INDEX_NAME}; +use std::str::FromStr; + +#[derive(Debug, Args)] +pub struct InitCommand {} + +impl InitCommand { + pub fn run(self, project: Project) -> anyhow::Result<()> { + match project.read_manifest() { + Ok(_) => { + println!("{}", "project already initialized".red()); + Ok(()) + } + Err(ManifestReadError::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => { + let mut manifest = nondestructive::yaml::from_slice(b"").unwrap(); + let mut mapping = manifest.as_mut().make_mapping(); + + mapping.insert_str( + "name", + inquire::Text::new("What is the name of the project?") + .with_validator(|name: &str| { + Ok(match PackageName::from_str(name) { + Ok(_) => Validation::Valid, + Err(e) => Validation::Invalid(e.to_string().into()), + }) + }) + .prompt() + .unwrap(), + ); + mapping.insert_str("version", "0.1.0"); + + let description = inquire::Text::new( + "What is the description of the project? (leave empty for none)", + ) + .prompt() + .unwrap(); + + if !description.is_empty() { + mapping.insert_str("description", description); + } + + let authors = inquire::Text::new( + "Who are the authors of this project? (leave empty for none, comma separated)", + ) + .prompt() + .unwrap(); + + let authors = authors + .split(',') + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .collect::>(); + + if !authors.is_empty() { + let mut authors_field = mapping + .insert("authors", nondestructive::yaml::Separator::Auto) + .make_sequence(); + + for author in authors { + authors_field.push_string(author); + } + } + + let repo = inquire::Text::new( + "What is the repository URL of this project? (leave empty for none)", + ) + .with_validator(|repo: &str| { + if repo.is_empty() { + return Ok(Validation::Valid); + } + + Ok(match url::Url::parse(repo) { + Ok(_) => Validation::Valid, + Err(e) => Validation::Invalid(e.to_string().into()), + }) + }) + .prompt() + .unwrap(); + if !repo.is_empty() { + mapping.insert_str("repository", repo); + } + + let license = inquire::Text::new( + "What is the license of this project? (leave empty for none)", + ) + .with_initial_value("MIT") + .prompt() + .unwrap(); + if !license.is_empty() { + mapping.insert_str("license", license); + } + + let mut indices = mapping + .insert("indices", nondestructive::yaml::Separator::Auto) + .make_mapping(); + indices.insert_str( + DEFAULT_INDEX_NAME, + read_config(project.data_dir())?.default_index.as_str(), + ); + + project.write_manifest(manifest.to_string())?; + + println!("{}", "initialized project".green()); + Ok(()) + } + Err(e) => Err(e.into()), + } + } +} diff --git a/src/cli/install.rs b/src/cli/install.rs new file mode 100644 index 0000000..833b2f6 --- /dev/null +++ b/src/cli/install.rs @@ -0,0 +1,13 @@ +use clap::Args; +use pesde::Project; + +#[derive(Debug, Args)] +pub struct InstallCommand {} + +impl InstallCommand { + pub fn run(self, project: Project) -> anyhow::Result<()> { + dbg!(project.dependency_graph(None)?); + + Ok(()) + } +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 87eeb8f..0175bdb 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,5 +1,3 @@ -use clap::Subcommand; - use anyhow::Context; use keyring::Entry; use pesde::Project; @@ -8,6 +6,9 @@ use std::path::Path; mod auth; mod config; +mod init; +mod install; +mod run; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CliConfig { @@ -124,8 +125,8 @@ pub fn reqwest_client(data_dir: &Path) -> anyhow::Result anyhow::Result<()> { match self { - SubCommand::Auth(auth) => auth.run(project), - SubCommand::Config(config) => config.run(project), + Subcommand::Auth(auth) => auth.run(project), + Subcommand::Config(config) => config.run(project), + Subcommand::Init(init) => init.run(project), + Subcommand::Run(run) => run.run(project), + Subcommand::Install(install) => install.run(project), } } } diff --git a/src/cli/run.rs b/src/cli/run.rs new file mode 100644 index 0000000..94f428f --- /dev/null +++ b/src/cli/run.rs @@ -0,0 +1,48 @@ +use anyhow::Context; +use clap::Args; +use pesde::{ + names::PackageName, + scripts::{execute_lune_script, execute_script}, + Project, +}; +use relative_path::RelativePathBuf; + +#[derive(Debug, Args)] +pub struct RunCommand { + /// The package name, script name, or path to a script to run + #[arg(index = 1)] + package_or_script: String, + + /// Arguments to pass to the script + #[arg(index = 2, last = true)] + args: Vec, +} + +impl RunCommand { + pub fn run(self, project: Project) -> anyhow::Result<()> { + if let Ok(_pkg_name) = self.package_or_script.parse::() { + todo!("implement binary package execution") + } + + if let Ok(manifest) = project.deser_manifest() { + if manifest.scripts.contains_key(&self.package_or_script) { + execute_script(&manifest, &self.package_or_script, &self.args) + .context("failed to execute script")?; + + return Ok(()); + } + }; + + let relative_path = RelativePathBuf::from(self.package_or_script); + let path = relative_path.to_path(project.path()); + + if !path.exists() { + anyhow::bail!("path does not exist: {}", path.display()); + } + + execute_lune_script(None, &relative_path, &self.args) + .context("failed to execute script")?; + + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 4875efd..e02a1ae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,16 +3,20 @@ #[cfg(not(any(feature = "roblox", feature = "lune", feature = "luau")))] compile_error!("at least one of the features `roblox`, `lune`, or `luau` must be enabled"); +use crate::lockfile::Lockfile; use once_cell::sync::Lazy; use std::path::{Path, PathBuf}; pub mod lockfile; pub mod manifest; pub mod names; +pub mod resolver; +pub mod scripts; pub mod source; pub const MANIFEST_FILE_NAME: &str = "pesde.yaml"; pub const LOCKFILE_FILE_NAME: &str = "pesde.lock"; +pub const DEFAULT_INDEX_NAME: &str = "default"; pub(crate) static REQWEST_CLIENT: Lazy = Lazy::new(|| { reqwest::blocking::Client::builder() @@ -143,6 +147,17 @@ impl Project { pub fn write_manifest>(&self, manifest: S) -> Result<(), std::io::Error> { std::fs::write(self.path.join(MANIFEST_FILE_NAME), manifest.as_ref()) } + + pub fn deser_lockfile(&self) -> Result { + let bytes = std::fs::read(self.path.join(LOCKFILE_FILE_NAME))?; + Ok(serde_yaml::from_slice(&bytes)?) + } + + pub fn write_lockfile(&self, lockfile: Lockfile) -> Result<(), errors::LockfileWriteError> { + let writer = std::fs::File::create(self.path.join(LOCKFILE_FILE_NAME))?; + serde_yaml::to_writer(writer, &lockfile)?; + Ok(()) + } } pub mod errors { @@ -157,4 +172,24 @@ pub mod errors { #[error("error deserializing manifest file")] Serde(#[from] serde_yaml::Error), } + + #[derive(Debug, Error)] + #[non_exhaustive] + pub enum LockfileReadError { + #[error("io error reading lockfile file")] + Io(#[from] std::io::Error), + + #[error("error deserializing lockfile file")] + Serde(#[from] serde_yaml::Error), + } + + #[derive(Debug, Error)] + #[non_exhaustive] + pub enum LockfileWriteError { + #[error("io error writing lockfile file")] + Io(#[from] std::io::Error), + + #[error("error serializing lockfile file")] + Serde(#[from] serde_yaml::Error), + } } diff --git a/src/lockfile.rs b/src/lockfile.rs index 126867a..e12d199 100644 --- a/src/lockfile.rs +++ b/src/lockfile.rs @@ -1,20 +1,60 @@ use crate::{ + manifest::{DependencyType, OverrideKey}, names::{PackageName, PackageNames}, source::{DependencySpecifiers, PackageRefs}, }; use semver::Version; use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; +use std::collections::{btree_map::Entry, BTreeMap}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct DependencyGraphNode { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub direct: Option<(String, DependencySpecifiers)>, + pub pkg_ref: PackageRefs, + pub dependencies: BTreeMap, + pub ty: DependencyType, +} + +pub type DependencyGraph = BTreeMap>; + +pub fn insert_node( + graph: &mut DependencyGraph, + name: PackageNames, + version: Version, + node: DependencyGraphNode, +) { + match graph + .entry(name.clone()) + .or_default() + .entry(version.clone()) + { + Entry::Vacant(entry) => { + entry.insert(node); + } + Entry::Occupied(existing) => { + let current_node = existing.into_mut(); + + match (¤t_node.direct, &node.direct) { + (Some(_), Some(_)) => { + log::warn!("duplicate direct dependency for {name}@{version}",); + } + + (None, Some(_)) => { + current_node.direct = node.direct; + } + + (_, _) => {} + } + } + } +} #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Lockfile { pub name: PackageName, + pub version: Version, + pub overrides: BTreeMap, - pub specifiers: BTreeMap>, - pub dependencies: BTreeMap>, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct LockfileNode { - pub pkg_ref: PackageRefs, + pub graph: DependencyGraph, } diff --git a/src/main.rs b/src/main.rs index 75253ed..4fd0012 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,10 +14,12 @@ struct Cli { version: (), #[command(subcommand)] - subcommand: cli::SubCommand, + subcommand: cli::Subcommand, } fn main() { + pretty_env_logger::init(); + let project_dirs = directories::ProjectDirs::from("com", env!("CARGO_PKG_NAME"), env!("CARGO_BIN_NAME")) .expect("couldn't get home directory"); diff --git a/src/manifest.rs b/src/manifest.rs index 520413a..8acb79f 100644 --- a/src/manifest.rs +++ b/src/manifest.rs @@ -1,7 +1,6 @@ -use crate::{names::PackageName, source::DependencySpecifiers}; use relative_path::RelativePathBuf; use semver::Version; -use serde::{de::Visitor, Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use serde_with::{DeserializeFromStr, SerializeDisplay}; use std::{ collections::BTreeMap, @@ -9,19 +8,11 @@ use std::{ str::FromStr, }; -#[derive(Serialize, Deserialize, Debug, Clone, Default)] -#[serde(deny_unknown_fields)] -pub struct Exports { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub lib: Option, - - #[serde(default, skip_serializing_if = "Option::is_none")] - pub bin: Option, -} +use crate::{names::PackageName, source::DependencySpecifiers}; #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] #[serde(rename_all = "snake_case", deny_unknown_fields)] -pub enum Target { +pub enum TargetKind { #[cfg(feature = "roblox")] Roblox, #[cfg(feature = "lune")] @@ -30,33 +21,111 @@ pub enum Target { Luau, } -impl Display for Target { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for TargetKind { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { #[cfg(feature = "roblox")] - Target::Roblox => write!(f, "roblox"), + TargetKind::Roblox => write!(f, "roblox"), #[cfg(feature = "lune")] - Target::Lune => write!(f, "lune"), + TargetKind::Lune => write!(f, "lune"), #[cfg(feature = "luau")] - Target::Luau => write!(f, "luau"), + TargetKind::Luau => write!(f, "luau"), } } } -impl Target { +impl TargetKind { // self is the project's target, dependency is the target of the dependency - fn is_compatible_with(&self, dependency: &Self) -> bool { + pub fn is_compatible_with(&self, dependency: &Self) -> bool { if self == dependency { return true; } match (self, dependency) { #[cfg(all(feature = "lune", feature = "luau"))] - (Target::Lune, Target::Luau) => true, + (TargetKind::Lune, TargetKind::Luau) => true, _ => false, } } + + pub fn packages_folder(&self, dependency: &Self) -> String { + if self == dependency { + return "packages".to_string(); + } + + format!("{}_packages", dependency) + } +} + +#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[serde(rename_all = "snake_case", tag = "environment", remote = "Self")] +pub enum Target { + #[cfg(feature = "roblox")] + Roblox { lib: RelativePathBuf }, + #[cfg(feature = "lune")] + Lune { + lib: Option, + bin: Option, + }, + #[cfg(feature = "luau")] + Luau { + lib: Option, + bin: Option, + }, +} + +impl<'de> Deserialize<'de> for Target { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let target = Self::deserialize(deserializer)?; + + match &target { + #[cfg(feature = "lune")] + Target::Lune { lib, bin } => { + if lib.is_none() && bin.is_none() { + return Err(serde::de::Error::custom( + "one of `lib` or `bin` exports must be defined", + )); + } + } + + #[cfg(feature = "luau")] + Target::Luau { lib, bin } => { + if lib.is_none() && bin.is_none() { + return Err(serde::de::Error::custom( + "one of `lib` or `bin` exports must be defined", + )); + } + } + + #[allow(unreachable_patterns)] + _ => {} + }; + + Ok(target) + } +} + +impl Display for Target { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.kind()) + } +} + +impl Target { + pub fn kind(&self) -> TargetKind { + match self { + #[cfg(feature = "roblox")] + Target::Roblox { .. } => TargetKind::Roblox, + #[cfg(feature = "lune")] + Target::Lune { .. } => TargetKind::Lune, + #[cfg(feature = "luau")] + Target::Luau { .. } => TargetKind::Luau, + } + } } #[derive( @@ -68,11 +137,16 @@ impl FromStr for OverrideKey { type Err = errors::OverrideKeyFromStr; fn from_str(s: &str) -> Result { - Ok(Self( - s.split(',') - .map(|overrides| overrides.split('>').map(|s| s.to_string()).collect()) - .collect(), - )) + let overrides = s + .split(',') + .map(|overrides| overrides.split('>').map(|s| s.to_string()).collect()) + .collect::>>(); + + if overrides.is_empty() { + return Err(errors::OverrideKeyFromStr::Empty); + } + + Ok(Self(overrides)) } } @@ -96,40 +170,7 @@ impl Display for OverrideKey { } } -fn deserialize_dep_specs<'de, D>( - deserializer: D, -) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - struct SpecsVisitor; - - impl<'de> Visitor<'de> for SpecsVisitor { - type Value = BTreeMap; - - fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { - formatter.write_str("a map of dependency specifiers") - } - - fn visit_map(self, mut map: A) -> Result - where - A: serde::de::MapAccess<'de>, - { - let mut specs = BTreeMap::new(); - - while let Some((key, mut value)) = map.next_entry::()? { - value.set_alias(key.to_string()); - specs.insert(key, value); - } - - Ok(specs) - } - } - - deserializer.deserialize_map(SpecsVisitor) -} - -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Deserialize, Debug, Clone)] pub struct Manifest { pub name: PackageName, pub version: Version, @@ -141,34 +182,77 @@ pub struct Manifest { pub authors: Option>, #[serde(default)] pub repository: Option, - #[serde(default)] - pub exports: Exports, pub target: Target, #[serde(default)] pub private: bool, #[serde(default)] + pub scripts: BTreeMap, + #[serde(default)] pub indices: BTreeMap, #[cfg(feature = "wally")] #[serde(default)] pub wally_indices: BTreeMap, - #[cfg(feature = "wally")] + #[cfg(all(feature = "wally", feature = "roblox"))] #[serde(default)] pub sourcemap_generator: Option, #[serde(default)] pub overrides: BTreeMap, - #[serde(default, deserialize_with = "deserialize_dep_specs")] + #[serde(default)] pub dependencies: BTreeMap, - #[serde(default, deserialize_with = "deserialize_dep_specs")] + #[serde(default)] pub peer_dependencies: BTreeMap, - #[serde(default, deserialize_with = "deserialize_dep_specs")] + #[serde(default)] pub dev_dependencies: BTreeMap, } +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[serde(rename_all = "snake_case")] +pub enum DependencyType { + Standard, + Dev, + Peer, +} + +impl Manifest { + pub fn all_dependencies( + &self, + ) -> Result< + BTreeMap, + errors::AllDependenciesError, + > { + let mut all_deps = BTreeMap::new(); + + for (deps, ty) in [ + (&self.dependencies, DependencyType::Standard), + (&self.peer_dependencies, DependencyType::Peer), + (&self.dev_dependencies, DependencyType::Dev), + ] { + for (alias, spec) in deps { + if all_deps.insert(alias.clone(), (spec.clone(), ty)).is_some() { + return Err(errors::AllDependenciesError::AliasConflict(alias.clone())); + } + } + } + + Ok(all_deps) + } +} + pub mod errors { use thiserror::Error; #[derive(Debug, Error)] #[non_exhaustive] - pub enum OverrideKeyFromStr {} + pub enum OverrideKeyFromStr { + #[error("empty override key")] + Empty, + } + + #[derive(Debug, Error)] + #[non_exhaustive] + pub enum AllDependenciesError { + #[error("another specifier is already using the alias {0}")] + AliasConflict(String), + } } diff --git a/src/names.rs b/src/names.rs index df2bfa3..ac1cfa8 100644 --- a/src/names.rs +++ b/src/names.rs @@ -70,6 +70,14 @@ pub enum PackageNames { Pesde(PackageName), } +impl Display for PackageNames { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PackageNames::Pesde(name) => write!(f, "{}", name), + } + } +} + pub mod errors { use thiserror::Error; diff --git a/src/resolver.rs b/src/resolver.rs new file mode 100644 index 0000000..225f690 --- /dev/null +++ b/src/resolver.rs @@ -0,0 +1,284 @@ +use crate::{ + lockfile::{insert_node, DependencyGraph, DependencyGraphNode}, + manifest::DependencyType, + names::PackageNames, + source::{ + pesde::PesdePackageSource, DependencySpecifiers, PackageRef, PackageSource, PackageSources, + }, + Project, DEFAULT_INDEX_NAME, +}; +use semver::Version; +use std::collections::{HashMap, HashSet, VecDeque}; + +impl Project { + // TODO: implement dependency overrides + pub fn dependency_graph( + &self, + previous_graph: Option<&DependencyGraph>, + ) -> Result> { + let manifest = self.deser_manifest().map_err(|e| Box::new(e.into()))?; + + let mut all_dependencies = manifest + .all_dependencies() + .map_err(|e| Box::new(e.into()))?; + + let mut all_specifiers = all_dependencies + .clone() + .into_iter() + .map(|(alias, (spec, ty))| ((spec, ty), alias)) + .collect::>(); + + let mut graph = DependencyGraph::default(); + + if let Some(previous_graph) = previous_graph { + for (name, versions) in previous_graph { + for (version, node) in versions { + let Some((_, specifier)) = &node.direct else { + // this is not a direct dependency, will be added if it's still being used later + continue; + }; + + match all_specifiers.remove(&(specifier.clone(), node.ty)) { + Some(alias) => { + all_dependencies.remove(&alias); + } + None => { + // this dependency is no longer in the manifest, or it's type has changed + continue; + } + } + + log::debug!("resolved {}@{} from old dependency graph", name, version); + insert_node(&mut graph, name.clone(), version.clone(), node.clone()); + + let mut queue = node + .dependencies + .iter() + .map(|(name, (version, _))| (name, version, 0usize)) + .collect::>(); + + while let Some((dep_name, dep_version, depth)) = queue.pop_front() { + if let Some(dep_node) = previous_graph + .get(dep_name) + .and_then(|v| v.get(dep_version)) + { + log::debug!( + "{}resolved dependency {}@{} from {}@{}", + "\t".repeat(depth), + dep_name, + dep_version, + name, + version + ); + insert_node( + &mut graph, + dep_name.clone(), + dep_version.clone(), + dep_node.clone(), + ); + + dep_node + .dependencies + .iter() + .map(|(name, (version, _))| (name, version, depth + 1)) + .for_each(|dep| queue.push_back(dep)); + } else { + log::warn!( + "dependency {}@{} from {}@{} not found in previous graph", + dep_name, + dep_version, + name, + version + ); + } + } + } + } + } + + let mut refreshed_sources = HashSet::new(); + let mut queue = all_dependencies + .into_iter() + .map(|(alias, (spec, ty))| (alias, spec, ty, None::<(PackageNames, Version)>, 0usize)) + .collect::>(); + + while let Some((alias, specifier, ty, dependant, depth)) = queue.pop_front() { + log::debug!( + "{}resolving {specifier} ({alias}) from {dependant:?}", + "\t".repeat(depth) + ); + let source = match &specifier { + DependencySpecifiers::Pesde(specifier) => { + let index_url = if depth == 0 { + let index_name = specifier.index.as_deref().unwrap_or(DEFAULT_INDEX_NAME); + let index_url = manifest.indices.get(index_name).ok_or( + errors::DependencyGraphError::IndexNotFound(index_name.to_string()), + )?; + + match index_url.as_str().try_into() { + Ok(url) => url, + Err(e) => { + return Err(Box::new(errors::DependencyGraphError::UrlParse( + index_url.clone(), + e, + ))) + } + } + } else { + let index_url = specifier.index.clone().unwrap(); + + index_url + .clone() + .try_into() + .map_err(|e| errors::DependencyGraphError::InvalidIndex(index_url, e))? + }; + + PackageSources::Pesde(PesdePackageSource::new(index_url)) + } + }; + + if refreshed_sources.insert(source.clone()) { + source.refresh(self).map_err(|e| Box::new(e.into()))?; + } + + let (name, resolved) = source + .resolve(&specifier, self) + .map_err(|e| Box::new(e.into()))?; + + let Some(target_version) = graph + .get(&name) + .and_then(|versions| { + versions + .keys() + // only consider versions that are compatible with the specifier + .filter(|ver| resolved.contains_key(ver)) + .max() + }) + .or_else(|| resolved.last_key_value().map(|(ver, _)| ver)) + .cloned() + else { + log::warn!( + "{}could not find any version for {specifier} ({alias})", + "\t".repeat(depth) + ); + continue; + }; + + let ty = if depth == 0 && ty == DependencyType::Peer { + DependencyType::Standard + } else { + ty + }; + + if let Some((dependant_name, dependant_version)) = dependant { + graph + .get_mut(&dependant_name) + .and_then(|versions| versions.get_mut(&dependant_version)) + .and_then(|node| { + node.dependencies + .insert(name.clone(), (target_version.clone(), alias.clone())) + }); + } + + if let Some(already_resolved) = graph + .get_mut(&name) + .and_then(|versions| versions.get_mut(&target_version)) + { + log::debug!( + "{}{}@{} already resolved", + "\t".repeat(depth), + name, + target_version + ); + + if already_resolved.ty == DependencyType::Peer && ty == DependencyType::Standard { + already_resolved.ty = ty; + } + + continue; + } + + let pkg_ref = &resolved[&target_version]; + let node = DependencyGraphNode { + direct: if depth == 0 { + Some((alias.clone(), specifier.clone())) + } else { + None + }, + pkg_ref: pkg_ref.clone(), + dependencies: Default::default(), + ty, + }; + insert_node( + &mut graph, + name.clone(), + target_version.clone(), + node.clone(), + ); + + log::debug!( + "{}resolved {}@{} from new dependency graph", + "\t".repeat(depth), + name, + target_version + ); + + for (dependency_alias, (dependency_spec, dependency_ty)) in + pkg_ref.dependencies().clone() + { + if dependency_ty == DependencyType::Dev { + // dev dependencies of dependencies are not included in the graph + // they should not even be stored in the index, so this is just a check to avoid potential issues + continue; + } + + queue.push_back(( + dependency_alias, + dependency_spec, + dependency_ty, + Some((name.clone(), target_version.clone())), + depth + 1, + )); + } + } + + for (name, versions) in &graph { + for (version, node) in versions { + if node.ty == DependencyType::Peer { + log::warn!("peer dependency {name}@{version} was not resolved"); + } + } + } + + Ok(graph) + } +} + +pub mod errors { + use thiserror::Error; + + #[derive(Debug, Error)] + #[non_exhaustive] + pub enum DependencyGraphError { + #[error("failed to deserialize manifest")] + ManifestRead(#[from] crate::errors::ManifestReadError), + + #[error("error getting all project dependencies")] + AllDependencies(#[from] crate::manifest::errors::AllDependenciesError), + + #[error("index named {0} not found in manifest")] + IndexNotFound(String), + + #[error("error parsing url {0} into git url")] + UrlParse(url::Url, #[source] gix::url::parse::Error), + + #[error("index {0} cannot be parsed as a git url")] + InvalidIndex(String, #[source] gix::url::parse::Error), + + #[error("error refreshing package source")] + Refresh(#[from] crate::source::errors::RefreshError), + + #[error("error resolving package")] + Resolve(#[from] crate::source::errors::ResolveError), + } +} diff --git a/src/scripts.rs b/src/scripts.rs new file mode 100644 index 0000000..e7552be --- /dev/null +++ b/src/scripts.rs @@ -0,0 +1,80 @@ +use crate::manifest::Manifest; +use relative_path::RelativePathBuf; +use std::{ + ffi::OsStr, + io::{BufRead, BufReader}, + process::{Command, Stdio}, + thread::spawn, +}; + +pub fn execute_lune_script, S: AsRef>( + script_name: Option<&str>, + script_path: &RelativePathBuf, + args: A, +) -> Result<(), std::io::Error> { + match Command::new("lune") + .arg("run") + .arg(script_path.as_str()) + .args(args) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + { + Ok(mut child) => { + let stdout = BufReader::new(child.stdout.take().unwrap()); + let stderr = BufReader::new(child.stderr.take().unwrap()); + + let script = match script_name { + Some(script) => script.to_string(), + None => script_path.to_string(), + }; + + let script_2 = script.to_string(); + + spawn(move || { + for line in stderr.lines() { + match line { + Ok(line) => { + log::error!("[{script}]: {line}"); + } + Err(e) => { + log::error!("ERROR IN READING STDERR OF {script}: {e}"); + break; + } + } + } + }); + + for line in stdout.lines() { + match line { + Ok(line) => { + log::info!("[{script_2}]: {line}"); + } + Err(e) => { + log::error!("ERROR IN READING STDOUT OF {script_2}: {e}"); + break; + } + } + } + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + log::warn!("Lune could not be found in PATH: {e}") + } + Err(e) => return Err(e), + }; + + Ok(()) +} + +pub fn execute_script, S: AsRef>( + manifest: &Manifest, + script: &str, + args: A, +) -> Result<(), std::io::Error> { + if let Some(script_path) = manifest.scripts.get(script) { + return execute_lune_script(Some(script), script_path, args); + } + + Ok(()) +} diff --git a/src/source/mod.rs b/src/source/mod.rs index 0e0e440..c6c1893 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -1,28 +1,59 @@ -use std::{collections::BTreeMap, fmt::Debug, path::Path}; - +use crate::{manifest::DependencyType, names::PackageNames, Project}; use semver::Version; use serde::{Deserialize, Serialize}; - -use crate::Project; +use std::{ + collections::BTreeMap, + fmt::{Debug, Display}, + path::Path, +}; pub mod pesde; -pub trait DependencySpecifier: Debug { - fn alias(&self) -> &str; - fn set_alias(&mut self, alias: String); -} - -pub trait PackageRef: Debug {} - pub(crate) fn hash(struc: &S) -> String { - use std::collections::hash_map::DefaultHasher; - use std::hash::Hasher; + use std::{collections::hash_map::DefaultHasher, hash::Hasher}; let mut hasher = DefaultHasher::new(); struc.hash(&mut hasher); hasher.finish().to_string() } +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] +#[serde(untagged)] +pub enum DependencySpecifiers { + Pesde(pesde::PesdeDependencySpecifier), +} +pub trait DependencySpecifier: Debug + Display {} +impl DependencySpecifier for DependencySpecifiers {} + +impl Display for DependencySpecifiers { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DependencySpecifiers::Pesde(specifier) => write!(f, "{}", specifier), + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum PackageRefs { + Pesde(pesde::PesdePackageRef), +} +pub trait PackageRef: Debug { + fn dependencies(&self) -> &BTreeMap; +} +impl PackageRef for PackageRefs { + fn dependencies(&self) -> &BTreeMap { + match self { + PackageRefs::Pesde(pkg_ref) => pkg_ref.dependencies(), + } + } +} + +pub type ResolveResult = (PackageNames, BTreeMap); + +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +pub enum PackageSources { + Pesde(pesde::PesdePackageSource), +} pub trait PackageSource: Debug { type Ref: PackageRef; type Specifier: DependencySpecifier; @@ -38,7 +69,7 @@ pub trait PackageSource: Debug { &self, specifier: &Self::Specifier, project: &Project, - ) -> Result, Self::ResolveError>; + ) -> Result, Self::ResolveError>; fn download( &self, @@ -47,33 +78,85 @@ pub trait PackageSource: Debug { project: &Project, ) -> Result<(), Self::DownloadError>; } +impl PackageSource for PackageSources { + type Ref = PackageRefs; + type Specifier = DependencySpecifiers; + type RefreshError = errors::RefreshError; + type ResolveError = errors::ResolveError; + type DownloadError = errors::DownloadError; -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] -#[serde(untagged)] -pub enum DependencySpecifiers { - Pesde(pesde::PesdeDependencySpecifier), -} - -impl DependencySpecifiers { - pub fn alias(&self) -> &str { + fn refresh(&self, project: &Project) -> Result<(), Self::RefreshError> { match self { - DependencySpecifiers::Pesde(spec) => spec.alias(), + PackageSources::Pesde(source) => source.refresh(project).map_err(Into::into), } } - pub fn set_alias(&mut self, alias: String) { - match self { - DependencySpecifiers::Pesde(spec) => spec.set_alias(alias), + fn resolve( + &self, + specifier: &Self::Specifier, + project: &Project, + ) -> Result, Self::ResolveError> { + match (self, specifier) { + (PackageSources::Pesde(source), DependencySpecifiers::Pesde(specifier)) => source + .resolve(specifier, project) + .map(|(name, results)| { + ( + name, + results + .into_iter() + .map(|(version, pkg_ref)| (version, PackageRefs::Pesde(pkg_ref))) + .collect(), + ) + }) + .map_err(Into::into), + + _ => Err(errors::ResolveError::Mismatch), + } + } + + fn download( + &self, + pkg_ref: &Self::Ref, + destination: &Path, + project: &Project, + ) -> Result<(), Self::DownloadError> { + match (self, pkg_ref) { + (PackageSources::Pesde(source), PackageRefs::Pesde(pkg_ref)) => source + .download(pkg_ref, destination, project) + .map_err(Into::into), + + _ => Err(errors::DownloadError::Mismatch), } } } -#[derive(Debug, Serialize, Deserialize, Clone)] -pub enum PackageRefs { - Pesde(pesde::PesdePackageRef), -} +pub mod errors { + use thiserror::Error; -#[derive(Debug, Eq, PartialEq, Hash)] -pub enum PackageSources { - Pesde(pesde::PesdePackageSource), + #[derive(Debug, Error)] + #[non_exhaustive] + pub enum RefreshError { + #[error("error refreshing pesde package source")] + Pesde(#[from] crate::source::pesde::errors::RefreshError), + } + + #[derive(Debug, Error)] + #[non_exhaustive] + pub enum ResolveError { + #[error("mismatched dependency specifier for source")] + Mismatch, + + #[error("error resolving pesde package")] + Pesde(#[from] crate::source::pesde::errors::ResolveError), + } + + #[derive(Debug, Error)] + #[non_exhaustive] + pub enum DownloadError { + #[error("mismatched package ref for source")] + Mismatch, + + #[error("error downloading pesde package")] + Pesde(#[from] crate::source::pesde::errors::DownloadError), + } } diff --git a/src/source/pesde.rs b/src/source/pesde.rs index 324031c..5568083 100644 --- a/src/source/pesde.rs +++ b/src/source/pesde.rs @@ -1,4 +1,9 @@ -use std::{collections::BTreeMap, fmt::Debug, hash::Hash, path::Path}; +use std::{ + collections::BTreeMap, + fmt::{Debug, Display}, + hash::Hash, + path::Path, +}; use gix::remote::Direction; use semver::{Version, VersionReq}; @@ -6,9 +11,11 @@ use serde::{Deserialize, Serialize}; use crate::{ authenticate_conn, - manifest::Target, - names::PackageName, - source::{hash, DependencySpecifier, DependencySpecifiers, PackageRef, PackageSource}, + manifest::{DependencyType, TargetKind}, + names::{PackageName, PackageNames}, + source::{ + hash, DependencySpecifier, DependencySpecifiers, PackageRef, PackageSource, ResolveResult, + }, Project, REQWEST_CLIENT, }; @@ -16,17 +23,14 @@ use crate::{ pub struct PesdeDependencySpecifier { pub name: PackageName, pub version: VersionReq, - #[serde(default, skip_serializing_if = "String::is_empty")] - pub alias: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub index: Option, } +impl DependencySpecifier for PesdeDependencySpecifier {} -impl DependencySpecifier for PesdeDependencySpecifier { - fn alias(&self) -> &str { - self.alias.as_str() - } - - fn set_alias(&mut self, alias: String) { - self.alias = alias; +impl Display for PesdeDependencySpecifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}@{}", self.name, self.version) } } @@ -34,9 +38,14 @@ impl DependencySpecifier for PesdeDependencySpecifier { pub struct PesdePackageRef { name: PackageName, version: Version, + index_url: gix::Url, + dependencies: BTreeMap, +} +impl PackageRef for PesdePackageRef { + fn dependencies(&self) -> &BTreeMap { + &self.dependencies + } } - -impl PackageRef for PesdePackageRef {} impl Ord for PesdePackageRef { fn cmp(&self, other: &Self) -> std::cmp::Ordering { @@ -50,7 +59,7 @@ impl PartialOrd for PesdePackageRef { } } -#[derive(Debug, Hash, PartialEq, Eq)] +#[derive(Debug, Hash, PartialEq, Eq, Clone)] pub struct PesdePackageSource { repo_url: gix::Url, } @@ -326,30 +335,35 @@ impl PackageSource for PesdePackageSource { &self, specifier: &Self::Specifier, project: &Project, - ) -> Result, Self::ResolveError> { + ) -> Result, Self::ResolveError> { let (scope, name) = specifier.name.as_str(); let bytes = match self.read_file([scope, name], project) { Ok(Some(bytes)) => bytes, - Ok(None) => return Ok(BTreeMap::new()), + Ok(None) => return Err(Self::ResolveError::NotFound(specifier.name.to_string())), Err(e) => return Err(Self::ResolveError::Read(specifier.name.to_string(), e)), }; let entries: Vec = serde_yaml::from_slice(&bytes) .map_err(|e| Self::ResolveError::Parse(specifier.name.to_string(), e))?; - Ok(entries - .into_iter() - .filter(|entry| specifier.version.matches(&entry.version)) - .map(|entry| { - ( - entry.version.clone(), - PesdePackageRef { - name: specifier.name.clone(), - version: entry.version, - }, - ) - }) - .collect()) + Ok(( + PackageNames::Pesde(specifier.name.clone()), + entries + .into_iter() + .filter(|entry| specifier.version.matches(&entry.version)) + .map(|entry| { + ( + entry.version.clone(), + PesdePackageRef { + name: specifier.name.clone(), + version: entry.version, + index_url: self.repo_url.clone(), + dependencies: entry.dependencies, + }, + ) + }) + .collect(), + )) } fn download( @@ -418,15 +432,15 @@ impl IndexConfig { #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] pub struct IndexFileEntry { pub version: Version, - pub target: Target, + pub target: TargetKind, #[serde(default = "chrono::Utc::now")] pub published_at: chrono::DateTime, #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub dependencies: Vec, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub dependencies: BTreeMap, } impl Ord for IndexFileEntry { @@ -457,28 +471,28 @@ pub mod errors { Io(#[from] std::io::Error), #[error("error opening repository at {0}")] - Open(PathBuf, gix::open::Error), + Open(PathBuf, #[source] gix::open::Error), #[error("no default remote found in repository at {0}")] NoDefaultRemote(PathBuf), #[error("error getting default remote from repository at {0}")] - GetDefaultRemote(PathBuf, gix::remote::find::existing::Error), + GetDefaultRemote(PathBuf, #[source] gix::remote::find::existing::Error), #[error("error connecting to remote repository at {0}")] - Connect(gix::Url, gix::remote::connect::Error), + Connect(gix::Url, #[source] gix::remote::connect::Error), #[error("error preparing fetch from remote repository at {0}")] - PrepareFetch(gix::Url, gix::remote::fetch::prepare::Error), + PrepareFetch(gix::Url, #[source] gix::remote::fetch::prepare::Error), #[error("error reading from remote repository at {0}")] - Read(gix::Url, gix::remote::fetch::Error), + Read(gix::Url, #[source] gix::remote::fetch::Error), #[error("error cloning repository from {0}")] - Clone(gix::Url, gix::clone::Error), + Clone(gix::Url, #[source] gix::clone::Error), #[error("error fetching repository from {0}")] - Fetch(gix::Url, gix::clone::fetch::Error), + Fetch(gix::Url, #[source] gix::clone::fetch::Error), } #[derive(Debug, Error)] @@ -488,13 +502,13 @@ pub mod errors { Io(#[from] std::io::Error), #[error("error opening repository at {0}")] - Open(PathBuf, gix::open::Error), + Open(PathBuf, #[source] gix::open::Error), #[error("no default remote found in repository at {0}")] NoDefaultRemote(PathBuf), #[error("error getting default remote from repository at {0}")] - GetDefaultRemote(PathBuf, gix::remote::find::existing::Error), + GetDefaultRemote(PathBuf, #[source] gix::remote::find::existing::Error), #[error("no refspecs found in repository at {0}")] NoRefSpecs(PathBuf), @@ -503,29 +517,29 @@ pub mod errors { NoLocalRefSpec(PathBuf), #[error("no reference found for local refspec {0}")] - NoReference(String, gix::reference::find::existing::Error), + NoReference(String, #[source] gix::reference::find::existing::Error), #[error("cannot peel reference {0}")] - CannotPeel(String, gix::reference::peel::Error), + CannotPeel(String, #[source] gix::reference::peel::Error), #[error("error converting id {0} to object")] - CannotConvertToObject(String, gix::object::find::existing::Error), + CannotConvertToObject(String, #[source] gix::object::find::existing::Error), #[error("error peeling object {0} to tree")] - CannotPeelToTree(String, gix::object::peel::to_kind::Error), + CannotPeelToTree(String, #[source] gix::object::peel::to_kind::Error), } #[derive(Debug, Error)] #[non_exhaustive] pub enum ReadFile { #[error("error opening repository at {0}")] - Open(PathBuf, gix::open::Error), + Open(PathBuf, #[source] gix::open::Error), #[error("error getting tree from repository at {0}")] - Tree(PathBuf, Box), + Tree(PathBuf, #[source] Box), #[error("error looking up entry {0} in tree")] - Lookup(String, gix::object::find::existing::Error), + Lookup(String, #[source] gix::object::find::existing::Error), } #[derive(Debug, Error)] @@ -534,11 +548,14 @@ pub mod errors { #[error("error interacting with the filesystem")] Io(#[from] std::io::Error), + #[error("package {0} not found")] + NotFound(String), + #[error("error reading file for {0}")] - Read(String, Box), + Read(String, #[source] Box), #[error("error parsing file for {0}")] - Parse(String, serde_yaml::Error), + Parse(String, #[source] serde_yaml::Error), } #[derive(Debug, Error)] @@ -558,19 +575,19 @@ pub mod errors { #[non_exhaustive] pub enum AllPackagesError { #[error("error opening repository at {0}")] - Open(PathBuf, gix::open::Error), + Open(PathBuf, #[source] gix::open::Error), #[error("error getting tree from repository at {0}")] - Tree(PathBuf, Box), + Tree(PathBuf, #[source] Box), #[error("error decoding entry in repository at {0}")] - Decode(PathBuf, gix::objs::decode::Error), + Decode(PathBuf, #[source] gix::objs::decode::Error), #[error("error converting entry in repository at {0}")] - Convert(PathBuf, gix::object::find::existing::Error), + Convert(PathBuf, #[source] gix::object::find::existing::Error), #[error("error deserializing file {0} in repository at {1}")] - Deserialize(String, PathBuf, serde_yaml::Error), + Deserialize(String, PathBuf, #[source] serde_yaml::Error), } #[derive(Debug, Error)]