From 9e1ffc41b6bd1b4e92ac6578a0ae7a6b1edc30ce Mon Sep 17 00:00:00 2001 From: daimond113 <72147841+daimond113@users.noreply.github.com> Date: Sat, 16 Mar 2024 22:36:12 +0100 Subject: [PATCH] feat: :children_crossing: add wally conversion --- Cargo.lock | 2 +- Cargo.toml | 17 ++-- src/cli/root.rs | 32 ++++--- src/dependencies/git.rs | 8 +- src/dependencies/mod.rs | 4 +- src/index.rs | 29 +++++-- src/main.rs | 3 + src/manifest.rs | 179 +++++++++++++++++++++++++++++++++++++++- src/multithread.rs | 30 +++---- tests/prelude.rs | 9 +- 10 files changed, 254 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 97a7716..3fdf260 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3275,6 +3275,7 @@ dependencies = [ "tempfile", "thiserror", "threadpool", + "toml", ] [[package]] @@ -3287,7 +3288,6 @@ dependencies = [ "actix-multipart-derive", "actix-web", "actix-web-httpauth", - "chrono", "dotenvy", "flate2", "git2", diff --git a/Cargo.toml b/Cargo.toml index 6da0fd9..6a72a95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,34 +19,35 @@ required-features = ["bin"] [dependencies] serde = { version = "1.0.197", features = ["derive"] } serde_yaml = "0.9.32" +toml = "0.8.11" git2 = "0.18.2" semver = { version = "1.0.22", features = ["serde"] } -reqwest = { version = "0.11.24", default-features = false, features = ["rustls-tls", "blocking"] } +reqwest = { version = "0.11.26", default-features = false, features = ["rustls-tls", "blocking"] } tar = "0.4.40" flate2 = "1.0.28" pathdiff = "0.2.1" relative-path = { version = "1.9.2", features = ["serde"] } -log = "0.4.20" -thiserror = "1.0.57" +log = "0.4.21" +thiserror = "1.0.58" threadpool = "1.8.1" full_moon = { version = "0.19.0", features = ["stacker", "roblox"] } # chrono-lc breaks because of https://github.com/chronotope/chrono/compare/v0.4.34...v0.4.35#diff-67de5678fb5c14378bbff7ecf7f8bfab17cc223c4726f8da3afca183a4e59543 chrono = { version = "=0.4.34", features = ["serde"] } -clap = { version = "4.5.1", features = ["derive"], optional = true } +clap = { version = "4.5.3", features = ["derive"], optional = true } directories = { version = "5.0.1", optional = true } keyring = { version = "2.3.2", optional = true } -anyhow = { version = "1.0.80", optional = true } +anyhow = { version = "1.0.81", optional = true } ignore = { version = "0.4.22", optional = true } pretty_env_logger = { version = "0.5.0", optional = true } serde_json = { version = "1.0.114", optional = true } -lune = { version = "0.8.0", optional = true } +lune = { version = "0.8.2", optional = true } futures-executor = { version = "0.3.30", optional = true } indicatif = { version = "0.17.8", optional = true } -auth-git2 = { version = "0.5.3", optional = true } +auth-git2 = { version = "0.5.4", optional = true } indicatif-log-bridge = { version = "0.2.2", optional = true } -inquire = { version = "0.7.0", optional = true } +inquire = { version = "0.7.1", optional = true } [dev-dependencies] tempfile = "3.10.1" diff --git a/src/cli/root.rs b/src/cli/root.rs index e078cfd..43b82e8 100644 --- a/src/cli/root.rs +++ b/src/cli/root.rs @@ -307,17 +307,21 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> { anyhow::bail!("manifest already exists"); } - let default_name = params.cwd.file_name().unwrap().to_str().unwrap(); + let default_name = params.cwd.file_name().and_then(|s| s.to_str()); - let name = Text::new("What is the name of the package?") - .with_initial_value(default_name) - .with_validator(|name: &str| { + let mut name = + Text::new("What is the name of the package?").with_validator(|name: &str| { Ok(match PackageName::from_str(name) { Ok(_) => Validation::Valid, Err(e) => Validation::Invalid(e.into()), }) - }) - .prompt()?; + }); + + if let Some(name_str) = default_name { + name = name.with_initial_value(name_str); + } + + let name = name.prompt()?; let path_style = Select::new("What style of paths do you want to use?", vec!["roblox"]).prompt()?; @@ -438,13 +442,12 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> { )))? .json::()? .as_array() - .unwrap() - .iter() - .map(|v| Version::parse(v.as_str().unwrap())) - .collect::, semver::Error>>()? - .into_iter() - .max() - .unwrap(); + .and_then(|a| a.last()) + .and_then(|v| v.as_str()) + .and_then(|s| s.parse::().ok()) + .ok_or(anyhow::anyhow!( + "failed to get latest version of {name}@{version}" + ))?; if latest_version > version { println!( @@ -455,6 +458,9 @@ pub fn root_command(cmd: Command, params: CliParams) -> anyhow::Result<()> { } } } + Command::Convert => { + Manifest::from_path_or_convert(¶ms.cwd)?; + } _ => unreachable!(), } diff --git a/src/dependencies/git.rs b/src/dependencies/git.rs index cb4f2e5..a7e83ea 100644 --- a/src/dependencies/git.rs +++ b/src/dependencies/git.rs @@ -8,7 +8,7 @@ use thiserror::Error; use crate::{ index::{remote_callbacks, Index}, - manifest::{Manifest, ManifestReadError, Realm}, + manifest::{Manifest, ManifestConvertError, Realm}, package_name::PackageName, project::Project, }; @@ -53,7 +53,7 @@ pub enum GitDownloadError { /// An error that occurred while reading the manifest of the git repository #[error("error reading manifest")] - ManifestRead(#[from] ManifestReadError), + ManifestRead(#[from] ManifestConvertError), } impl GitDependencySpecifier { @@ -120,7 +120,7 @@ impl GitDependencySpecifier { repo.reset(&obj, git2::ResetType::Hard, None)?; Ok(( - Manifest::from_path(dest)?, + Manifest::from_path_or_convert(dest)?, repo_url.to_string(), obj.id().to_string(), )) @@ -152,6 +152,8 @@ impl GitPackageRef { } repo.reset(&obj, git2::ResetType::Hard, None)?; + + Manifest::from_path_or_convert(dest)?; Ok(()) } diff --git a/src/dependencies/mod.rs b/src/dependencies/mod.rs index 4b5e327..a5ff9fc 100644 --- a/src/dependencies/mod.rs +++ b/src/dependencies/mod.rs @@ -143,7 +143,7 @@ impl Project { &self, map: &ResolvedVersionsMap, ) -> Result, InstallProjectError> { - let job = MultithreadedJob::new(); + let (job, tx) = MultithreadedJob::new(); for (name, versions) in map.clone() { for (version, resolved_package) in versions { @@ -163,7 +163,7 @@ impl Project { let project = self.clone(); - job.execute(move || resolved_package.pkg_ref.download(&project, source)); + job.execute(&tx, move || resolved_package.pkg_ref.download(&project, source)); } } diff --git a/src/index.rs b/src/index.rs index 8173cf8..2bb6be0 100644 --- a/src/index.rs +++ b/src/index.rs @@ -43,7 +43,7 @@ pub trait Index: Send + Sync + Debug + Clone + 'static { &mut self, manifest: &Manifest, uploader: &u64, - ) -> Result; + ) -> Result, CreatePackageVersionError>; /// Gets the index's configuration fn config(&self) -> Result; @@ -421,7 +421,7 @@ impl Index for GitIndex { &mut self, manifest: &Manifest, uploader: &u64, - ) -> Result { + ) -> Result, CreatePackageVersionError> { let scope = manifest.name.scope(); if let Some(owners) = self.scope_owners(scope)? { @@ -436,14 +436,15 @@ impl Index for GitIndex { let mut file = if let Some(file) = self.package(&manifest.name)? { if file.iter().any(|e| e.version == manifest.version) { - return Ok(false); + return Ok(None); } file } else { - vec![] + BTreeSet::new() }; - file.push(manifest.clone().into()); + let entry: IndexFileEntry = manifest.clone().into(); + file.insert(entry.clone()); serde_yaml::to_writer( std::fs::File::create(path.join(manifest.name.name()))?, @@ -451,7 +452,7 @@ impl Index for GitIndex { ) .map_err(CreatePackageVersionError::FileSer)?; - Ok(true) + Ok(Some(entry)) } fn config(&self) -> Result { @@ -515,7 +516,7 @@ impl IndexConfig { } /// An entry in the index file -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] pub struct IndexFileEntry { /// The version of the package pub version: Version, @@ -550,5 +551,17 @@ impl From for IndexFileEntry { } } +impl PartialOrd for IndexFileEntry { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.version.cmp(&other.version)) + } +} + +impl Ord for IndexFileEntry { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.version.cmp(&other.version) + } +} + /// An index file -pub type IndexFile = Vec; +pub type IndexFile = BTreeSet; diff --git a/src/main.rs b/src/main.rs index acd5912..fb83c44 100644 --- a/src/main.rs +++ b/src/main.rs @@ -106,6 +106,9 @@ pub enum Command { /// Publishes the project to the registry Publish, + /// Converts a `wally.toml` file to a `pesde.yaml` file + Convert, + /// Begins a new patch Patch { /// The package to patch diff --git a/src/manifest.rs b/src/manifest.rs index 031bba5..92bea9f 100644 --- a/src/manifest.rs +++ b/src/manifest.rs @@ -1,11 +1,14 @@ +use std::fs::read_to_string; +use std::path::PathBuf; use std::str::FromStr; use std::{collections::BTreeMap, fmt::Display, fs::read}; use relative_path::RelativePathBuf; -use semver::Version; +use semver::{Version, VersionReq}; use serde::{Deserialize, Serialize}; use thiserror::Error; +use crate::dependencies::registry::RegistryDependencySpecifier; use crate::{dependencies::DependencySpecifier, package_name::PackageName, MANIFEST_FILE_NAME}; /// The files exported by the package @@ -159,6 +162,42 @@ pub enum ManifestReadError { ManifestDeser(#[source] serde_yaml::Error), } +/// An error that occurred while converting the manifest +#[derive(Debug, Error)] +pub enum ManifestConvertError { + /// An error that occurred while reading the manifest + #[error("error reading the manifest")] + ManifestRead(#[from] ManifestReadError), + + /// An error that occurred while converting the manifest + #[error("error converting the manifest")] + ManifestConvert(#[source] toml::de::Error), + + /// The given path does not have a parent + #[error("the path {0} does not have a parent")] + NoParent(PathBuf), + + /// An error that occurred while interacting with the file system + #[error("error interacting with the file system")] + Io(#[from] std::io::Error), + + /// An error that occurred while making a package name from a string + #[error("error making a package name from a string")] + PackageName(#[from] crate::package_name::FromStrPackageNameParseError), + + /// An error that occurred while writing the manifest + #[error("error writing the manifest")] + ManifestWrite(#[from] serde_yaml::Error), + + /// An error that occurred while converting a dependency specifier's version + #[error("error converting a dependency specifier's version")] + Version(#[from] semver::Error), + + /// The dependency specifier isn't in the format of `scope/name@version` + #[error("the dependency specifier {0} isn't in the format of `scope/name@version`")] + InvalidDependencySpecifier(String), +} + /// The type of dependency #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] #[serde(rename_all = "snake_case")] @@ -187,6 +226,144 @@ impl Manifest { Ok(manifest) } + /// Tries to read the manifest from the given path, and if it fails, tries converting the `wally.toml` and writes a `pesde.yaml` in the same directory + pub fn from_path_or_convert>( + path: P, + ) -> Result { + let dir_path = if path.as_ref().file_name() == Some(MANIFEST_FILE_NAME.as_ref()) { + path.as_ref() + .parent() + .ok_or_else(|| ManifestConvertError::NoParent(path.as_ref().to_path_buf()))? + .to_path_buf() + } else { + path.as_ref().to_path_buf() + }; + + Self::from_path(path).or_else(|_| { + #[derive(Deserialize)] + struct WallyPackage { + name: String, + version: Version, + #[serde(default)] + realm: Option, + #[serde(default)] + description: Option, + #[serde(default)] + license: Option, + #[serde(default)] + authors: Option>, + #[serde(default)] + private: Option, + } + + #[derive(Deserialize, Default)] + struct WallyPlace { + #[serde(default)] + shared_packages: Option, + #[serde(default)] + server_packages: Option, + } + + #[derive(Deserialize)] + struct WallyDependencySpecifier(String); + + impl TryFrom for DependencySpecifier { + type Error = ManifestConvertError; + + fn try_from(specifier: WallyDependencySpecifier) -> Result { + let (name, req) = specifier.0.split_once('@').ok_or_else(|| { + ManifestConvertError::InvalidDependencySpecifier(specifier.0.clone()) + })?; + let name: PackageName = name.replace('-', "_").parse()?; + let req: VersionReq = req.parse()?; + + Ok(DependencySpecifier::Registry(RegistryDependencySpecifier { + name, + version: req, + realm: None, + })) + } + } + + #[derive(Deserialize)] + struct WallyManifest { + package: WallyPackage, + #[serde(default)] + place: WallyPlace, + #[serde(default)] + dependencies: BTreeMap, + #[serde(default)] + server_dependencies: BTreeMap, + #[serde(default)] + dev_dependencies: BTreeMap, + } + + let toml_path = dir_path.join("wally.toml"); + let toml_contents = read_to_string(toml_path)?; + let wally_manifest: WallyManifest = + toml::from_str(&toml_contents).map_err(ManifestConvertError::ManifestConvert)?; + + let mut place = BTreeMap::new(); + + if let Some(shared) = wally_manifest.place.shared_packages { + if !shared.is_empty() { + place.insert(Realm::Shared, shared); + } + } + + if let Some(server) = wally_manifest.place.server_packages { + if !server.is_empty() { + place.insert(Realm::Server, server); + } + } + + let manifest = Self { + name: wally_manifest.package.name.replace('-', "_").parse()?, + version: wally_manifest.package.version, + exports: Exports::default(), + path_style: PathStyle::Roblox { place }, + private: wally_manifest.package.private.unwrap_or(false), + realm: wally_manifest + .package + .realm + .map(|r| r.parse().unwrap_or(Realm::Shared)), + dependencies: [ + (wally_manifest.dependencies, Realm::Shared), + (wally_manifest.server_dependencies, Realm::Server), + (wally_manifest.dev_dependencies, Realm::Development), + ] + .into_iter() + .flat_map(|(deps, realm)| { + deps.into_values() + .map(|specifier| { + specifier.try_into().map(|mut specifier| { + match specifier { + DependencySpecifier::Registry(ref mut specifier) => { + specifier.realm = Some(realm); + } + _ => unreachable!(), + } + + specifier + }) + }) + .collect::>() + }) + .collect::>()?, + peer_dependencies: Vec::new(), + description: wally_manifest.package.description, + license: wally_manifest.package.license, + authors: wally_manifest.package.authors, + repository: None, + }; + + let manifest_path = dir_path.join(MANIFEST_FILE_NAME); + serde_yaml::to_writer(std::fs::File::create(manifest_path)?, &manifest)?; + + Ok(manifest) + }) + } + /// Returns all dependencies pub fn dependencies(&self) -> Vec<(DependencySpecifier, DependencyType)> { self.dependencies diff --git a/src/multithread.rs b/src/multithread.rs index 61b8bf8..7273594 100644 --- a/src/multithread.rs +++ b/src/multithread.rs @@ -4,27 +4,19 @@ use threadpool::ThreadPool; /// A multithreaded job pub struct MultithreadedJob { progress: Receiver>, - sender: Sender>, pool: ThreadPool, } -impl Default for MultithreadedJob { - fn default() -> Self { - let (tx, rx) = std::sync::mpsc::channel(); - let pool = ThreadPool::new(6); - - Self { - progress: rx, - pool, - sender: tx.clone(), - } - } -} - impl MultithreadedJob { /// Creates a new multithreaded job - pub fn new() -> Self { - Self::default() + pub fn new() -> (Self, Sender>) { + let (tx, rx) = std::sync::mpsc::channel(); + let pool = ThreadPool::new(6); + + (Self { + progress: rx, + pool, + }, tx) } /// Returns the progress of the job @@ -44,12 +36,12 @@ impl MultithreadedJob { } /// Executes a function on the thread pool - pub fn execute(&self, f: F) + pub fn execute(&self, tx: &Sender>, f: F) where F: (FnOnce() -> Result<(), E>) + Send + 'static, { - let sender = self.sender.clone(); - + let sender = tx.clone(); + self.pool.execute(move || { let result = f(); sender.send(result).unwrap(); diff --git a/tests/prelude.rs b/tests/prelude.rs index 4072f3b..38ff3fe 100644 --- a/tests/prelude.rs +++ b/tests/prelude.rs @@ -34,7 +34,7 @@ impl InMemoryIndex { .entry(scope.to_string()) .or_insert_with(|| (BTreeSet::new(), IndexFile::default())) .1 - .push(index_file); + .insert(index_file); self } } @@ -65,7 +65,7 @@ impl Index for InMemoryIndex { &mut self, manifest: &Manifest, uploader: &u64, - ) -> Result { + ) -> Result, CreatePackageVersionError> { let scope = manifest.name.scope(); if let Some(owners) = self.scope_owners(scope)? { @@ -78,9 +78,10 @@ impl Index for InMemoryIndex { let package = self.packages.get_mut(scope).unwrap(); - package.1.push(manifest.clone().into()); + let entry: IndexFileEntry = manifest.clone().into(); + package.1.insert(entry.clone()); - Ok(true) + Ok(Some(entry)) } fn config(&self) -> Result {