feat: add roblox server target

This commit is contained in:
daimond113 2024-09-06 23:38:44 +02:00
parent 30c4d0c391
commit 10c804e2f3
No known key found for this signature in database
GPG key ID: 3A8ECE51328B513C
10 changed files with 232 additions and 75 deletions

View file

@ -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<HttpResponse, Error> {
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<HttpResponse, Error> {
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<HttpResponse, Error> {
let object_url = PutObject::new(
let object_url = GetObject::new(
&self.s3_bucket,
Some(&self.s3_credentials),
&format!("doc/{}.gz", doc_hash),

View file

@ -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"))]

View file

@ -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<String, errors::GetLibRequirePath> {
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::<Vec<_>>()
.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),
}
}

View file

@ -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),
}
}

View file

@ -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<String>,
#[cfg(feature = "roblox")]
/// The Roblox place of this project
#[serde(default, skip_serializing)]
pub place: BTreeMap<target::RobloxPlaceKind, String>,
/// The standard dependencies of the package
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]

View file

@ -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<String>,
},
/// A Roblox server target
#[cfg(feature = "roblox")]
RobloxServer {
/// The path to the lib export file
#[serde(default)]
lib: Option<RelativePathBuf>,
/// The files to include in the sync tool's config
#[serde(default)]
build_files: BTreeSet<String>,
},
/// 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<String>> {
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<RobloxPlaceKind> for &TargetKind {
type Error = ();
fn try_into(self) -> Result<RobloxPlaceKind, Self::Error> {
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]

View file

@ -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;
}

View file

@ -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)
})? {

View file

@ -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<Target, errors::FindLibPathError> {
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),
}
}

View file

@ -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>>(