diff --git a/registry/src/storage/s3.rs b/registry/src/storage/s3.rs index 5cfc64b..3168b03 100644 --- a/registry/src/storage/s3.rs +++ b/registry/src/storage/s3.rs @@ -2,7 +2,10 @@ use crate::{error::Error, storage::StorageImpl}; use actix_web::{http::header::LOCATION, HttpResponse}; use pesde::{names::PackageName, source::version_id::VersionId}; use reqwest::header::{CONTENT_ENCODING, CONTENT_TYPE}; -use rusty_s3::{actions::PutObject, Bucket, Credentials, S3Action}; +use rusty_s3::{ + actions::{GetObject, PutObject}, + Bucket, Credentials, S3Action, +}; use std::{fmt::Display, time::Duration}; #[derive(Debug)] @@ -48,7 +51,7 @@ impl StorageImpl for S3Storage { package_name: &PackageName, version: &VersionId, ) -> Result { - let object_url = PutObject::new( + let object_url = GetObject::new( &self.s3_bucket, Some(&self.s3_credentials), &format!( @@ -97,7 +100,7 @@ impl StorageImpl for S3Storage { package_name: &PackageName, version: &VersionId, ) -> Result { - let object_url = PutObject::new( + let object_url = GetObject::new( &self.s3_bucket, Some(&self.s3_credentials), &format!( @@ -133,7 +136,7 @@ impl StorageImpl for S3Storage { } async fn get_doc(&self, doc_hash: &str) -> Result { - let object_url = PutObject::new( + let object_url = GetObject::new( &self.s3_bucket, Some(&self.s3_credentials), &format!("doc/{}.gz", doc_hash), diff --git a/src/cli/commands/publish.rs b/src/cli/commands/publish.rs index 77e4525..d55a705 100644 --- a/src/cli/commands/publish.rs +++ b/src/cli/commands/publish.rs @@ -1,17 +1,17 @@ -use std::{ - io::{Seek, Write}, - path::Component, -}; - use anyhow::Context; use clap::Args; use colored::Colorize; use reqwest::StatusCode; use semver::VersionReq; +use std::{ + io::{Seek, Write}, + path::Component, +}; use tempfile::tempfile; +use crate::cli::up_to_date_lockfile; use pesde::{ - manifest::target::Target, + manifest::{target::Target, DependencyType}, scripts::ScriptName, source::{ pesde::{specifier::PesdeDependencySpecifier, PesdePackageSource}, @@ -42,10 +42,39 @@ impl PublishCommand { return Ok(()); } - manifest - .target - .validate_publish() - .context("manifest not fit for publishing")?; + if manifest.target.lib_path().is_none() && manifest.target.bin_path().is_none() { + anyhow::bail!("no exports found in target"); + } + + #[cfg(feature = "roblox")] + if matches!( + manifest.target, + Target::Roblox { .. } | Target::RobloxServer { .. } + ) { + if !manifest.target.build_files().is_some_and(|f| !f.is_empty()) { + anyhow::bail!("no build files found in target"); + } + + match up_to_date_lockfile(&project)? { + Some(lockfile) => { + if lockfile + .graph + .values() + .flatten() + .filter_map(|(_, node)| node.node.direct.as_ref().map(|_| node)) + .any(|node| { + node.target.build_files().is_none() + && !matches!(node.node.ty, DependencyType::Dev) + }) + { + anyhow::bail!("roblox packages may not depend on non-roblox packages"); + } + } + None => { + anyhow::bail!("outdated lockfile, please run the install command first") + } + } + } let mut archive = tar::Builder::new(flate2::write::GzEncoder::new( vec![], @@ -65,6 +94,7 @@ impl PublishCommand { #[cfg(feature = "roblox")] let mut roblox_target = match &mut manifest.target { Target::Roblox { build_files, .. } => Some(build_files), + Target::RobloxServer { build_files, .. } => Some(build_files), _ => None, }; #[cfg(not(feature = "roblox"))] diff --git a/src/linking/generator.rs b/src/linking/generator.rs index 8a3a9dd..f70f4bb 100644 --- a/src/linking/generator.rs +++ b/src/linking/generator.rs @@ -1,6 +1,6 @@ use std::path::{Component, Path}; -use crate::manifest::target::TargetKind; +use crate::manifest::{target::TargetKind, Manifest}; use full_moon::{ast::luau::ExportedTypeDeclaration, visitors::Visitor}; use relative_path::RelativePathBuf; @@ -82,14 +82,20 @@ fn luau_style_path(path: &Path) -> String { format!("{require:?}") } +// This function should be simplified (especially to reduce the number of arguments), +// but it's not clear how to do that while maintaining the current functionality. /// Get the require path for a library +#[allow(clippy::too_many_arguments)] pub fn get_lib_require_path( target: &TargetKind, base_dir: &Path, lib_file: &RelativePathBuf, destination_dir: &Path, use_new_structure: bool, -) -> String { + root_container_dir: &Path, + container_dir: &Path, + project_manifest: &Manifest, +) -> Result { let path = pathdiff::diff_paths(destination_dir, base_dir).unwrap(); let path = if use_new_structure { log::debug!("using new structure for require path with {:?}", lib_file); @@ -100,7 +106,25 @@ pub fn get_lib_require_path( }; #[cfg(feature = "roblox")] - if matches!(target, TargetKind::Roblox) { + if matches!(target, TargetKind::Roblox | TargetKind::RobloxServer) { + let (prefix, path) = match target.try_into() { + Ok(place_kind) if !destination_dir.starts_with(root_container_dir) => ( + project_manifest + .place + .get(&place_kind) + .ok_or(errors::GetLibRequirePath::RobloxPlaceKindPathNotFound( + place_kind, + ))? + .as_str(), + if use_new_structure { + lib_file.to_path(container_dir) + } else { + container_dir.to_path_buf() + }, + ), + _ => ("script.Parent", path), + }; + let path = path .components() .filter_map(|component| match component { @@ -118,10 +142,10 @@ pub fn get_lib_require_path( .collect::>() .join(""); - return format!("script.Parent{path}"); + return Ok(format!("{prefix}{path}")); }; - luau_style_path(&path) + Ok(luau_style_path(&path)) } /// Generate a linking module for a binary @@ -144,3 +168,17 @@ pub fn get_bin_require_path( luau_style_path(&path) } + +/// Errors for the linking module utilities +pub mod errors { + use thiserror::Error; + + /// An error occurred while getting the require path for a library + #[derive(Debug, Error)] + pub enum GetLibRequirePath { + /// The path for the RobloxPlaceKind could not be found + #[cfg(feature = "roblox")] + #[error("could not find the path for the RobloxPlaceKind {0}")] + RobloxPlaceKindPathNotFound(crate::manifest::target::RobloxPlaceKind), + } +} diff --git a/src/linking/mod.rs b/src/linking/mod.rs index 7212b07..3ad459f 100644 --- a/src/linking/mod.rs +++ b/src/linking/mod.rs @@ -1,7 +1,6 @@ use crate::{ linking::generator::get_file_types, lockfile::DownloadedGraph, - manifest::target::Target, names::PackageNames, scripts::{execute_script, ScriptName}, source::{fs::store_in_cas, traits::PackageRef, version_id::VersionId}, @@ -92,8 +91,9 @@ impl Project { .insert(version_id, types); #[cfg(feature = "roblox")] - if let Some(Target::Roblox { build_files, .. }) = - Some(&node.target).filter(|_| !node.node.pkg_ref.like_wally()) + if let Some(build_files) = Some(&node.target) + .filter(|_| !node.node.pkg_ref.like_wally()) + .and_then(|t| t.build_files()) { let script_name = ScriptName::RobloxSyncConfigGenerator.to_string(); @@ -122,7 +122,7 @@ impl Project { for (name, versions) in graph { for (version_id, node) in versions { - let node_container_folder = { + let (node_container_folder, node_packages_folder) = { let base_folder = create_and_canonicalize( self.package_dir().join( manifest @@ -158,7 +158,10 @@ impl Project { lib_file, &container_folder, node.node.pkg_ref.use_new_structure(), - ), + &base_folder, + container_folder.strip_prefix(&base_folder).unwrap(), + &manifest, + )?, types, ), )?; @@ -180,7 +183,7 @@ impl Project { } } - container_folder + (container_folder, base_folder) }; for (dependency_name, (dependency_version_id, dependency_alias)) in @@ -200,15 +203,21 @@ impl Project { continue; }; - let packages_container_folder = create_and_canonicalize( + let base_folder = create_and_canonicalize( self.package_dir().join( node.node .pkg_ref .target_kind() .packages_folder(&dependency_node.node.pkg_ref.target_kind()), ), - )? - .join(PACKAGES_CONTAINER_NAME); + )?; + let packages_container_folder = base_folder.join(PACKAGES_CONTAINER_NAME); + + let container_folder = dependency_node.node.container_folder( + &packages_container_folder, + dependency_name, + dependency_version_id.version(), + ); let linker_folder = create_and_canonicalize( node_container_folder @@ -223,13 +232,12 @@ impl Project { &dependency_node.target.kind(), &linker_folder, lib_file, - &dependency_node.node.container_folder( - &packages_container_folder, - dependency_name, - dependency_version_id.version(), - ), + &container_folder, dependency_node.node.pkg_ref.use_new_structure(), - ), + &node_packages_folder, + container_folder.strip_prefix(&base_folder).unwrap(), + &manifest, + )?, package_types .get(dependency_name) .and_then(|v| v.get(dependency_version_id)) @@ -276,5 +284,9 @@ pub mod errors { #[cfg(feature = "roblox")] #[error("error generating roblox sync config for {0}")] GenerateRobloxSyncConfig(String, #[source] std::io::Error), + + /// An error occurred while getting the require path for a library + #[error("error getting require path for library")] + GetLibRequirePath(#[from] super::generator::errors::GetLibRequirePath), } } diff --git a/src/manifest/mod.rs b/src/manifest/mod.rs index 57e0c58..de1d232 100644 --- a/src/manifest/mod.rs +++ b/src/manifest/mod.rs @@ -77,6 +77,10 @@ pub struct Manifest { /// A list of globs pointing to workspace members' directories #[serde(default, skip_serializing_if = "Vec::is_empty")] pub workspace_members: Vec, + #[cfg(feature = "roblox")] + /// The Roblox place of this project + #[serde(default, skip_serializing)] + pub place: BTreeMap, /// The standard dependencies of the package #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] diff --git a/src/manifest/target.rs b/src/manifest/target.rs index 073870d..9dc5fd9 100644 --- a/src/manifest/target.rs +++ b/src/manifest/target.rs @@ -1,5 +1,6 @@ use relative_path::RelativePathBuf; use serde::{Deserialize, Serialize}; +use serde_with::{DeserializeFromStr, SerializeDisplay}; use std::{ collections::BTreeSet, fmt::{Display, Formatter}, @@ -7,12 +8,16 @@ use std::{ }; /// A kind of target -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] -#[serde(rename_all = "snake_case", deny_unknown_fields)] +#[derive( + SerializeDisplay, DeserializeFromStr, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, +)] pub enum TargetKind { /// A Roblox target #[cfg(feature = "roblox")] Roblox, + /// A Roblox server target + #[cfg(feature = "roblox")] + RobloxServer, /// A Lune target #[cfg(feature = "lune")] Lune, @@ -26,6 +31,8 @@ impl Display for TargetKind { match self { #[cfg(feature = "roblox")] TargetKind::Roblox => write!(f, "roblox"), + #[cfg(feature = "roblox")] + TargetKind::RobloxServer => write!(f, "roblox_server"), #[cfg(feature = "lune")] TargetKind::Lune => write!(f, "lune"), #[cfg(feature = "luau")] @@ -41,6 +48,8 @@ impl FromStr for TargetKind { match s { #[cfg(feature = "roblox")] "roblox" => Ok(Self::Roblox), + #[cfg(feature = "roblox")] + "roblox_server" => Ok(Self::RobloxServer), #[cfg(feature = "lune")] "lune" => Ok(Self::Lune), #[cfg(feature = "luau")] @@ -55,6 +64,8 @@ impl TargetKind { pub const VARIANTS: &'static [TargetKind] = &[ #[cfg(feature = "roblox")] TargetKind::Roblox, + #[cfg(feature = "roblox")] + TargetKind::RobloxServer, #[cfg(feature = "lune")] TargetKind::Lune, #[cfg(feature = "luau")] @@ -72,6 +83,9 @@ impl TargetKind { #[cfg(all(feature = "lune", feature = "luau"))] (TargetKind::Lune, TargetKind::Luau) => true, + #[cfg(feature = "roblox")] + (TargetKind::RobloxServer, TargetKind::Roblox) => true, + _ => false, } } @@ -101,6 +115,16 @@ pub enum Target { #[serde(default)] build_files: BTreeSet, }, + /// A Roblox server target + #[cfg(feature = "roblox")] + RobloxServer { + /// The path to the lib export file + #[serde(default)] + lib: Option, + /// The files to include in the sync tool's config + #[serde(default)] + build_files: BTreeSet, + }, /// A Lune target #[cfg(feature = "lune")] Lune { @@ -129,6 +153,8 @@ impl Target { match self { #[cfg(feature = "roblox")] Target::Roblox { .. } => TargetKind::Roblox, + #[cfg(feature = "roblox")] + Target::RobloxServer { .. } => TargetKind::RobloxServer, #[cfg(feature = "lune")] Target::Lune { .. } => TargetKind::Lune, #[cfg(feature = "luau")] @@ -141,6 +167,8 @@ impl Target { match self { #[cfg(feature = "roblox")] Target::Roblox { lib, .. } => lib.as_ref(), + #[cfg(feature = "roblox")] + Target::RobloxServer { lib, .. } => lib.as_ref(), #[cfg(feature = "lune")] Target::Lune { lib, .. } => lib.as_ref(), #[cfg(feature = "luau")] @@ -153,6 +181,8 @@ impl Target { match self { #[cfg(feature = "roblox")] Target::Roblox { .. } => None, + #[cfg(feature = "roblox")] + Target::RobloxServer { .. } => None, #[cfg(feature = "lune")] Target::Lune { bin, .. } => bin.as_ref(), #[cfg(feature = "luau")] @@ -160,28 +190,14 @@ impl Target { } } - /// Validates the target for publishing - pub fn validate_publish(&self) -> Result<(), errors::TargetValidatePublishError> { - let has_exports = match self { - #[cfg(feature = "roblox")] - Target::Roblox { lib, .. } => lib.is_some(), - #[cfg(feature = "lune")] - Target::Lune { lib, bin } => lib.is_some() || bin.is_some(), - #[cfg(feature = "luau")] - Target::Luau { lib, bin } => lib.is_some() || bin.is_some(), - }; - - if !has_exports { - return Err(errors::TargetValidatePublishError::NoExportedFiles); - } - + /// Returns the Roblox build files + pub fn build_files(&self) -> Option<&BTreeSet> { match self { #[cfg(feature = "roblox")] - Target::Roblox { build_files, .. } if build_files.is_empty() => { - Err(errors::TargetValidatePublishError::NoBuildFiles) - } - - _ => Ok(()), + Target::Roblox { build_files, .. } => Some(build_files), + #[cfg(feature = "roblox")] + Target::RobloxServer { build_files, .. } => Some(build_files), + _ => None, } } } @@ -192,24 +208,46 @@ impl Display for Target { } } +#[cfg(feature = "roblox")] +/// The kind of a Roblox place property +#[derive( + SerializeDisplay, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, +)] +#[serde(rename_all = "snake_case")] +pub enum RobloxPlaceKind { + /// The shared dependencies location + Shared, + /// The server dependencies location + Server, +} + +#[cfg(feature = "roblox")] +impl TryInto for &TargetKind { + type Error = (); + + fn try_into(self) -> Result { + match self { + TargetKind::Roblox => Ok(RobloxPlaceKind::Shared), + TargetKind::RobloxServer => Ok(RobloxPlaceKind::Server), + _ => Err(()), + } + } +} + +#[cfg(feature = "roblox")] +impl Display for RobloxPlaceKind { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + RobloxPlaceKind::Shared => write!(f, "shared"), + RobloxPlaceKind::Server => write!(f, "server"), + } + } +} + /// Errors that can occur when working with targets pub mod errors { use thiserror::Error; - /// Errors that can occur when validating a target for publishing - #[derive(Debug, Error)] - #[non_exhaustive] - pub enum TargetValidatePublishError { - /// No exported files specified - #[error("no exported files specified")] - NoExportedFiles, - - /// Roblox target must have at least one build file - #[cfg(feature = "roblox")] - #[error("roblox target must have at least one build file")] - NoBuildFiles, - } - /// Errors that can occur when parsing a target kind from a string #[derive(Debug, Error)] #[non_exhaustive] diff --git a/src/resolver.rs b/src/resolver.rs index 524f823..dedae81 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -255,6 +255,10 @@ impl Project { already_resolved.ty = ty; } + if already_resolved.direct.is_none() && depth == 0 { + already_resolved.direct = Some((alias.clone(), specifier.clone())); + } + continue; } diff --git a/src/source/git/mod.rs b/src/source/git/mod.rs index 2059a4e..0077f8a 100644 --- a/src/source/git/mod.rs +++ b/src/source/git/mod.rs @@ -257,7 +257,11 @@ impl PackageSource for GitPackageSource { #[cfg(feature = "wally-compat")] None => { match self - .read_file(["wally.toml"], project, Some(tree.clone())) + .read_file( + [crate::source::wally::compat_util::WALLY_MANIFEST_FILE_NAME], + project, + Some(tree.clone()), + ) .map_err(|e| { errors::ResolveError::ReadManifest(Box::new(self.repo_url.clone()), e) })? { diff --git a/src/source/wally/compat_util.rs b/src/source/wally/compat_util.rs index 4226eb5..44b5ee2 100644 --- a/src/source/wally/compat_util.rs +++ b/src/source/wally/compat_util.rs @@ -7,6 +7,7 @@ use tempfile::TempDir; use crate::{ manifest::target::Target, scripts::{execute_script, ScriptName}, + source::wally::manifest::{Realm, WallyManifest}, Project, LINK_LIB_NO_FILE_FOUND, }; @@ -50,14 +51,24 @@ pub(crate) fn find_lib_path( } } +pub(crate) const WALLY_MANIFEST_FILE_NAME: &str = "wally.toml"; + pub(crate) fn get_target( project: &Project, tempdir: &TempDir, ) -> Result { - Ok(Target::Roblox { - lib: find_lib_path(project, tempdir.path())? - .or_else(|| Some(RelativePathBuf::from(LINK_LIB_NO_FILE_FOUND))), - build_files: Default::default(), + let lib = find_lib_path(project, tempdir.path())? + .or_else(|| Some(RelativePathBuf::from(LINK_LIB_NO_FILE_FOUND))); + let build_files = Default::default(); + + let manifest = tempdir.path().join(WALLY_MANIFEST_FILE_NAME); + let manifest = std::fs::read_to_string(&manifest)?; + let manifest: WallyManifest = toml::from_str(&manifest)?; + + Ok(if matches!(manifest.package.realm, Realm::Shared) { + Target::Roblox { lib, build_files } + } else { + Target::RobloxServer { lib, build_files } }) } @@ -79,5 +90,9 @@ pub mod errors { /// An error occurred while deserializing the sourcemap result #[error("error deserializing sourcemap result")] Serde(#[from] serde_json::Error), + + /// An error occurred while deserializing the wally manifest + #[error("error deserializing wally manifest")] + WallyManifest(#[from] toml::de::Error), } } diff --git a/src/source/wally/manifest.rs b/src/source/wally/manifest.rs index 1ec44bf..9845f48 100644 --- a/src/source/wally/manifest.rs +++ b/src/source/wally/manifest.rs @@ -9,12 +9,21 @@ use crate::{ source::{specifiers::DependencySpecifiers, wally::specifier::WallyDependencySpecifier}, }; +#[derive(Deserialize, Clone, Debug)] +#[serde(rename_all = "lowercase")] +pub enum Realm { + #[serde(alias = "dev")] + Shared, + Server, +} + #[derive(Deserialize, Clone, Debug)] #[serde(rename_all = "kebab-case")] pub struct WallyPackage { pub name: WallyPackageName, pub version: Version, pub registry: url::Url, + pub realm: Realm, } pub fn deserialize_specifiers<'de, D: Deserializer<'de>>(