From 14463b72052a2ef794e149a39026229d9d0ce18f Mon Sep 17 00:00:00 2001 From: daimond113 <72147841+daimond113@users.noreply.github.com> Date: Mon, 22 Jul 2024 22:00:09 +0200 Subject: [PATCH] feat: implement dependency overrides --- Cargo.lock | 41 ------ Cargo.toml | 2 +- src/cli/init.rs | 242 ++++++++++++++++++------------------ src/cli/install.rs | 2 +- src/cli/mod.rs | 11 +- src/cli/run.rs | 2 +- src/lib.rs | 2 +- src/lockfile.rs | 1 + src/main.rs | 2 +- src/resolver.rs | 39 +++++- src/source/pesde/mod.rs | 2 +- src/source/pesde/pkg_ref.rs | 5 + src/{git.rs => util.rs} | 13 ++ 13 files changed, 186 insertions(+), 178 deletions(-) rename src/{git.rs => util.rs} (62%) diff --git a/Cargo.lock b/Cargo.lock index 98b7d51..3601b9e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -304,9 +304,6 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" -dependencies = [ - "serde", -] [[package]] name = "block-buffer" @@ -1109,7 +1106,6 @@ dependencies = [ "gix-index", "gix-lock", "gix-macros", - "gix-mailmap", "gix-negotiate", "gix-object", "gix-odb", @@ -1136,7 +1132,6 @@ dependencies = [ "once_cell", "parking_lot", "regex", - "serde", "smallvec", "thiserror", ] @@ -1151,7 +1146,6 @@ dependencies = [ "gix-date", "gix-utils", "itoa", - "serde", "thiserror", "winnow 0.6.14", ] @@ -1168,7 +1162,6 @@ dependencies = [ "gix-quote", "gix-trace", "kstring", - "serde", "smallvec", "thiserror", "unicode-bom", @@ -1215,7 +1208,6 @@ dependencies = [ "gix-features", "gix-hash", "memmap2", - "serde", "thiserror", ] @@ -1267,7 +1259,6 @@ dependencies = [ "gix-sec", "gix-trace", "gix-url", - "serde", "thiserror", ] @@ -1279,7 +1270,6 @@ checksum = "9eed6931f21491ee0aeb922751bd7ec97b4b2fe8fbfedcb678e2a2dce5f3b8c0" dependencies = [ "bstr", "itoa", - "serde", "thiserror", "time", ] @@ -1374,7 +1364,6 @@ dependencies = [ "bstr", "gix-features", "gix-path", - "serde", ] [[package]] @@ -1384,7 +1373,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93d7df7366121b5018f947a04d37f034717e113dcf9ccd85c34b58e57a74d5e" dependencies = [ "faster-hex", - "serde", "thiserror", ] @@ -1409,7 +1397,6 @@ dependencies = [ "gix-glob", "gix-path", "gix-trace", - "serde", "unicode-bom", ] @@ -1437,7 +1424,6 @@ dependencies = [ "libc", "memmap2", "rustix", - "serde", "smallvec", "thiserror", ] @@ -1464,19 +1450,6 @@ dependencies = [ "syn 2.0.71", ] -[[package]] -name = "gix-mailmap" -version = "0.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cb2da346958252cbc8656529f5479830a3bc6046f3d86405c9e77f71dfdf7b2" -dependencies = [ - "bstr", - "gix-actor", - "gix-date", - "serde", - "thiserror", -] - [[package]] name = "gix-negotiate" version = "0.13.1" @@ -1507,7 +1480,6 @@ dependencies = [ "gix-utils", "gix-validate", "itoa", - "serde", "smallvec", "thiserror", "winnow 0.6.14", @@ -1529,7 +1501,6 @@ dependencies = [ "gix-path", "gix-quote", "parking_lot", - "serde", "tempfile", "thiserror", ] @@ -1550,7 +1521,6 @@ dependencies = [ "gix-tempfile", "memmap2", "parking_lot", - "serde", "smallvec", "thiserror", ] @@ -1634,7 +1604,6 @@ dependencies = [ "gix-transport", "gix-utils", "maybe-async", - "serde", "thiserror", "winnow 0.6.14", ] @@ -1668,7 +1637,6 @@ dependencies = [ "gix-utils", "gix-validate", "memmap2", - "serde", "thiserror", "winnow 0.6.14", ] @@ -1700,7 +1668,6 @@ dependencies = [ "gix-object", "gix-revwalk", "gix-trace", - "serde", "thiserror", ] @@ -1728,7 +1695,6 @@ dependencies = [ "bitflags 2.6.0", "gix-path", "libc", - "serde", "windows-sys 0.52.0", ] @@ -1782,7 +1748,6 @@ dependencies = [ "gix-sec", "gix-url", "reqwest", - "serde", "thiserror", ] @@ -1813,7 +1778,6 @@ dependencies = [ "gix-features", "gix-path", "home", - "serde", "thiserror", "url", ] @@ -1855,7 +1819,6 @@ dependencies = [ "gix-object", "gix-path", "gix-validate", - "serde", ] [[package]] @@ -2284,7 +2247,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec3066350882a1cd6d950d055997f379ac37fd39f81cd4d8ed186032eb3c5747" dependencies = [ - "serde", "static_assertions", ] @@ -3356,9 +3318,6 @@ name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" -dependencies = [ - "serde", -] [[package]] name = "smol_str" diff --git a/Cargo.toml b/Cargo.toml index 5e5b96d..ec07c29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ serde = { version = "1.0.204", features = ["derive"] } toml = "0.8.15" serde_json = "1.0.120" serde_with = "3.9.0" -gix = { version = "0.63.0", default-features = false, features = ["blocking-http-transport-reqwest-rust-tls", "revparse-regex", "credentials", "serde"] } +gix = { version = "0.63.0", default-features = false, features = ["blocking-http-transport-reqwest-rust-tls", "revparse-regex", "credentials"] } semver = { version = "1.0.23", features = ["serde"] } reqwest = { version = "0.12.5", default-features = false, features = ["rustls-tls", "blocking"] } tar = "0.4.41" diff --git a/src/cli/init.rs b/src/cli/init.rs index 8f152ca..662693e 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -32,143 +32,141 @@ impl InitCommand { match project.read_manifest() { Ok(_) => { println!("{}", "project already initialized".red()); - Ok(()) + return Ok(()); } - Err(ManifestReadError::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => { - let mut manifest = toml_edit::DocumentMut::new(); + Err(ManifestReadError::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => {} + Err(e) => return Err(e.into()), + }; - manifest["name"] = toml_edit::value( - 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(), - ); - manifest["version"] = toml_edit::value("0.1.0"); + let mut manifest = toml_edit::DocumentMut::new(); - let description = inquire::Text::new( - "What is the description of the project? (leave empty for none)", - ) - .prompt() - .unwrap(); - - if !description.is_empty() { - manifest["description"] = toml_edit::value(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()) - .map(|s| s.into()) - .collect::>(); - - if !authors.is_empty() { - let mut authors_arr = toml_edit::Array::new(); - authors_arr.extend(authors); - - manifest["authors"] = toml_edit::value(authors_arr); - } - - 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) { + manifest["name"] = toml_edit::value( + 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(); - if !repo.is_empty() { - manifest["repository"] = toml_edit::value(repo); - } + .unwrap(), + ); + manifest["version"] = toml_edit::value("0.1.0"); - let license = inquire::Text::new( - "What is the license of this project? (leave empty for none)", - ) + let description = + inquire::Text::new("What is the description of the project? (leave empty for none)") + .prompt() + .unwrap(); + + if !description.is_empty() { + manifest["description"] = toml_edit::value(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()) + .map(|s| s.into()) + .collect::>(); + + if !authors.is_empty() { + let mut authors_arr = toml_edit::Array::new(); + authors_arr.extend(authors); + + manifest["authors"] = toml_edit::value(authors_arr); + } + + 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() { + manifest["repository"] = toml_edit::value(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() { - manifest["license"] = toml_edit::value(license); - } + if !license.is_empty() { + manifest["license"] = toml_edit::value(license); + } - let target_env = inquire::Select::new( - "What environment are you targeting for your package?", - vec![ - #[cfg(feature = "roblox")] - "roblox", - #[cfg(feature = "lune")] - "lune", - #[cfg(feature = "luau")] - "luau", - ], - ) - .prompt() + let target_env = inquire::Select::new( + "What environment are you targeting for your package?", + vec![ + #[cfg(feature = "roblox")] + "roblox", + #[cfg(feature = "lune")] + "lune", + #[cfg(feature = "luau")] + "luau", + ], + ) + .prompt() + .unwrap(); + + let mut target = toml_edit::Table::new(); + target["environment"] = toml_edit::value(target_env); + + if target_env == "roblox" + || inquire::Confirm::new(&format!( + "Would you like to setup a default {} script?", + ScriptName::RobloxSyncConfigGenerator + )) + .prompt() + .unwrap() + { + let folder = project.path().join(concat!(".", env!("CARGO_PKG_NAME"))); + std::fs::create_dir_all(&folder).context("failed to create scripts folder")?; + + std::fs::write( + folder.join(format!("{}.luau", ScriptName::RobloxSyncConfigGenerator)), + script_contents(Path::new(&format!( + "lune/rojo/{}.luau", + ScriptName::RobloxSyncConfigGenerator + ))), + ) + .context("failed to write script file")?; + + let scripts = manifest + .entry("scripts") + .or_insert(toml_edit::Item::Table(toml_edit::Table::new())) + .as_table_mut() .unwrap(); - let mut target = toml_edit::Table::new(); - target["environment"] = toml_edit::value(target_env); - - if target_env == "roblox" - || inquire::Confirm::new(&format!( - "Would you like to setup a default {} script?", - ScriptName::RobloxSyncConfigGenerator - )) - .prompt() - .unwrap() - { - let folder = project.path().join(concat!(".", env!("CARGO_PKG_NAME"))); - std::fs::create_dir_all(&folder).context("failed to create scripts folder")?; - - std::fs::write( - folder.join(format!("{}.luau", ScriptName::RobloxSyncConfigGenerator)), - script_contents(Path::new(&format!( - "lune/rojo/{}.luau", - ScriptName::RobloxSyncConfigGenerator - ))), - ) - .context("failed to write script file")?; - - let scripts = manifest - .entry("scripts") - .or_insert(toml_edit::Item::Table(toml_edit::Table::new())) - .as_table_mut() - .unwrap(); - - scripts[&ScriptName::RobloxSyncConfigGenerator.to_string()] = - toml_edit::value(format!( - concat!(".", env!("CARGO_PKG_NAME"), "/{}.luau"), - ScriptName::RobloxSyncConfigGenerator - )); - } - - let mut indices = toml_edit::Table::new(); - indices[DEFAULT_INDEX_NAME] = - toml_edit::value(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()), + scripts[&ScriptName::RobloxSyncConfigGenerator.to_string()] = + toml_edit::value(format!( + concat!(".", env!("CARGO_PKG_NAME"), "/{}.luau"), + ScriptName::RobloxSyncConfigGenerator + )); } + + let mut indices = toml_edit::Table::new(); + indices[DEFAULT_INDEX_NAME] = + toml_edit::value(read_config(project.data_dir())?.default_index.as_str()); + + project.write_manifest(manifest.to_string())?; + + println!("{}", "initialized project".green()); + Ok(()) } } diff --git a/src/cli/install.rs b/src/cli/install.rs index e0b1b79..b28521e 100644 --- a/src/cli/install.rs +++ b/src/cli/install.rs @@ -16,7 +16,7 @@ impl InstallCommand { .context("failed to read manifest")?; let lockfile = if project - .is_up_to_date() + .is_up_to_date(false) .context("failed to check if project is up to date")? { match project.deser_lockfile() { diff --git a/src/cli/mod.rs b/src/cli/mod.rs index a007ed2..5797143 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,4 +1,4 @@ -use crate::git::authenticate_conn; +use crate::util::authenticate_conn; use anyhow::Context; use gix::remote::Direction; use keyring::Entry; @@ -229,11 +229,11 @@ pub fn update_scripts_folder(project: &Project) -> anyhow::Result<()> { } pub trait IsUpToDate { - fn is_up_to_date(&self) -> anyhow::Result; + fn is_up_to_date(&self, strict: bool) -> anyhow::Result; } impl IsUpToDate for Project { - fn is_up_to_date(&self) -> anyhow::Result { + fn is_up_to_date(&self, strict: bool) -> anyhow::Result { let manifest = self.deser_manifest()?; let lockfile = match self.deser_lockfile() { Ok(lockfile) => lockfile, @@ -245,6 +245,11 @@ impl IsUpToDate for Project { Err(e) => return Err(e.into()), }; + if !strict { + // the resolver will use the old lockfile and update it itself. it can't handle overrides only + return Ok(manifest.overrides == lockfile.overrides); + } + if manifest.name != lockfile.name || manifest.version != lockfile.version { return Ok(false); } diff --git a/src/cli/run.rs b/src/cli/run.rs index 799f0ff..967e065 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -22,7 +22,7 @@ pub struct RunCommand { impl RunCommand { pub fn run(self, project: Project) -> anyhow::Result<()> { if let Ok(pkg_name) = self.package_or_script.parse::() { - let graph = if project.is_up_to_date()? { + let graph = if project.is_up_to_date(true)? { project.deser_lockfile()?.graph } else { anyhow::bail!("outdated lockfile, please run the install command first") diff --git a/src/lib.rs b/src/lib.rs index db8f11e..9f54061 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,6 @@ use once_cell::sync::Lazy; use std::path::{Path, PathBuf}; pub mod download; -mod git; pub mod linking; pub mod lockfile; pub mod manifest; @@ -16,6 +15,7 @@ pub mod names; pub mod resolver; pub mod scripts; pub mod source; +pub(crate) mod util; pub const MANIFEST_FILE_NAME: &str = "pesde.toml"; pub const LOCKFILE_FILE_NAME: &str = "pesde.lock"; diff --git a/src/lockfile.rs b/src/lockfile.rs index 89a2678..6d6187a 100644 --- a/src/lockfile.rs +++ b/src/lockfile.rs @@ -17,6 +17,7 @@ pub struct DependencyGraphNode { #[serde(default, skip_serializing_if = "Option::is_none")] pub direct: Option<(String, DependencySpecifiers)>, pub pkg_ref: PackageRefs, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub dependencies: BTreeMap, pub ty: DependencyType, } diff --git a/src/main.rs b/src/main.rs index e257011..df11597 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ use pesde::{AuthConfig, Project}; use std::fs::create_dir_all; mod cli; -pub mod git; +pub mod util; #[derive(Parser, Debug)] #[clap(version, about = "pesde is a feature-rich package manager for Luau")] diff --git a/src/resolver.rs b/src/resolver.rs index 9f9581b..5e23dd0 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -11,7 +11,7 @@ use semver::Version; use std::collections::{HashMap, HashSet, VecDeque}; impl Project { - // TODO: implement dependency overrides, account for targets using the is_compatible_with method + // TODO: account for targets using the is_compatible_with method pub fn dependency_graph( &self, previous_graph: Option<&DependencyGraph>, @@ -101,17 +101,28 @@ impl Project { let mut queue = all_specifiers .into_iter() - .map(|((spec, ty), alias)| (alias, spec, ty, None::<(PackageNames, Version)>, 0usize)) + .map(|((spec, ty), alias)| { + ( + alias.to_string(), + spec, + ty, + None::<(PackageNames, Version)>, + vec![alias.to_string()], + false, + ) + }) .collect::>(); - while let Some((alias, specifier, ty, dependant, depth)) = queue.pop_front() { + while let Some((alias, specifier, ty, dependant, path, overridden)) = queue.pop_front() { + let depth = path.len() - 1; + 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_url = if depth == 0 || overridden { 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()), @@ -235,12 +246,28 @@ impl Project { continue; } + let overridden = manifest.overrides.iter().find_map(|(key, spec)| { + key.0.iter().find_map(|override_path| { + // if the path up until the last element is the same as the current path, + // and the last element in the path is the dependency alias, + // then the specifier is to be overridden + (path.len() == override_path.len() - 1 + && path == override_path[..override_path.len() - 1] + && override_path.last() == Some(&dependency_alias)) + .then_some(spec) + }) + }); + queue.push_back(( dependency_alias, - dependency_spec, + overridden.cloned().unwrap_or(dependency_spec), dependency_ty, Some((name.clone(), target_version.clone())), - depth + 1, + path.iter() + .cloned() + .chain(std::iter::once(alias.to_string())) + .collect(), + overridden.is_some(), )); } } diff --git a/src/source/pesde/mod.rs b/src/source/pesde/mod.rs index 491411d..31ce726 100644 --- a/src/source/pesde/mod.rs +++ b/src/source/pesde/mod.rs @@ -15,10 +15,10 @@ use pkg_ref::PesdePackageRef; use specifier::PesdeDependencySpecifier; use crate::{ - git::authenticate_conn, manifest::{DependencyType, Target, TargetKind}, names::{PackageName, PackageNames}, source::{hash, DependencySpecifiers, PackageSource, ResolveResult}, + util::authenticate_conn, Project, REQWEST_CLIENT, }; diff --git a/src/source/pesde/pkg_ref.rs b/src/source/pesde/pkg_ref.rs index 9889863..9e1971e 100644 --- a/src/source/pesde/pkg_ref.rs +++ b/src/source/pesde/pkg_ref.rs @@ -13,7 +13,12 @@ use crate::{ pub struct PesdePackageRef { pub name: PackageName, pub version: Version, + #[serde( + serialize_with = "crate::util::serialize_gix_url", + deserialize_with = "crate::util::deserialize_gix_url" + )] pub index_url: gix::Url, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub dependencies: BTreeMap, pub target: Target, } diff --git a/src/git.rs b/src/util.rs similarity index 62% rename from src/git.rs rename to src/util.rs index 62f8e83..fe93afb 100644 --- a/src/git.rs +++ b/src/util.rs @@ -1,4 +1,6 @@ use crate::AuthConfig; +use gix::bstr::BStr; +use serde::{Deserialize, Deserializer, Serializer}; pub fn authenticate_conn( conn: &mut gix::remote::Connection< @@ -21,3 +23,14 @@ pub fn authenticate_conn( }); } } + +pub fn serialize_gix_url(url: &gix::Url, serializer: S) -> Result { + serializer.serialize_str(&url.to_bstring().to_string()) +} + +pub fn deserialize_gix_url<'de, D: Deserializer<'de>>( + deserializer: D, +) -> Result { + let s = String::deserialize(deserializer)?; + gix::Url::from_bytes(BStr::new(&s)).map_err(serde::de::Error::custom) +}