diff --git a/CHANGELOG.md b/CHANGELOG.md index 4360b29..93a58df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add improved CLI styling by @daimond113 - Install pesde dependencies before Wally to support scripts packages by @daimond113 +- Support packages exporting scripts by @daimond113 + +### Removed +- Remove special scripts repo handling to favour standard packages by @daimond113 ### Fixed - Link dependencies before type extraction to support more use cases by @daimond113 diff --git a/docs/src/components/SiteTitle.astro b/docs/src/components/SiteTitle.astro index c3da7d5..4ee93bc 100644 --- a/docs/src/components/SiteTitle.astro +++ b/docs/src/components/SiteTitle.astro @@ -3,7 +3,12 @@ href="https://pesde.daimond113.com/" class="flex text-[var(--sl-color-text-accent)] hover:opacity-80" > - + pesde - / + / docs/hello_pesde -# What is the description of the project? (leave empty for none) -# Who are the authors of this project? (leave empty for none, comma separated) -# What is the repository URL of this project? (leave empty for none) -# What is the license of this project? (leave empty for none) MIT -# What environment are you targeting for your package? luau -# Would you like to setup a default roblox_sync_config_generator script? No +# what is the name of the project? /hello_pesde +# what is the description of the project? +# who are the authors of this project? +# what is the repository URL of this project? +# what is the license of this project? MIT +# what environment are you targeting for your package? luau +# would you like to setup default Roblox compatibility scripts? No ``` The command will create a `pesde.toml` file in the current folder. Go ahead diff --git a/docs/src/content/docs/reference/cli.mdx b/docs/src/content/docs/reference/cli.mdx index 722b490..f24ca9e 100644 --- a/docs/src/content/docs/reference/cli.mdx +++ b/docs/src/content/docs/reference/cli.mdx @@ -55,19 +55,6 @@ is printed. The default index is [`pesde-index`](https://github.com/pesde-pkg/index). -### `pesde config scripts-repo` - -```sh -pesde config scripts-repo [REPO] -``` - -Configures the scripts repository. If no repository is provided, the current -scripts repository is printed. - -- `-r, --reset`: Resets the scripts repository. - -The default scripts repository is [`pesde-scripts`](https://github.com/pesde-pkg/scripts). - ## `pesde init` Initializes a new pesde project in the current directory. diff --git a/docs/src/content/docs/reference/manifest.mdx b/docs/src/content/docs/reference/manifest.mdx index 1e9cd16..f52351a 100644 --- a/docs/src/content/docs/reference/manifest.mdx +++ b/docs/src/content/docs/reference/manifest.mdx @@ -190,7 +190,7 @@ for various sync tools. ### `sourcemap_generator` @@ -205,7 +205,7 @@ through `process.args`. ## `[indices]` diff --git a/src/cli/commands/config/mod.rs b/src/cli/commands/config/mod.rs index a10570a..9b6cf75 100644 --- a/src/cli/commands/config/mod.rs +++ b/src/cli/commands/config/mod.rs @@ -1,22 +1,17 @@ use clap::Subcommand; mod default_index; -mod scripts_repo; #[derive(Debug, Subcommand)] pub enum ConfigCommands { /// Configuration for the default index DefaultIndex(default_index::DefaultIndexCommand), - - /// Configuration for the scripts repository - ScriptsRepo(scripts_repo::ScriptsRepoCommand), } impl ConfigCommands { pub async fn run(self) -> anyhow::Result<()> { match self { ConfigCommands::DefaultIndex(default_index) => default_index.run().await, - ConfigCommands::ScriptsRepo(scripts_repo) => scripts_repo.run().await, } } } diff --git a/src/cli/commands/config/scripts_repo.rs b/src/cli/commands/config/scripts_repo.rs deleted file mode 100644 index 16d6525..0000000 --- a/src/cli/commands/config/scripts_repo.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::cli::{ - config::{read_config, write_config, CliConfig}, - home_dir, -}; -use anyhow::Context; -use clap::Args; -use fs_err::tokio as fs; - -#[derive(Debug, Args)] -pub struct ScriptsRepoCommand { - /// The new repo URL to set as default, don't pass any value to check the current default repo - #[arg(index = 1, value_parser = crate::cli::parse_gix_url)] - repo: Option, - - /// Resets the default repo to the default value - #[arg(short, long, conflicts_with = "repo")] - reset: bool, -} - -impl ScriptsRepoCommand { - pub async fn run(self) -> anyhow::Result<()> { - let mut config = read_config().await?; - - let repo = if self.reset { - Some(CliConfig::default().scripts_repo) - } else { - self.repo - }; - - match repo { - Some(repo) => { - config.scripts_repo = repo.clone(); - write_config(&config).await?; - - fs::remove_dir_all(home_dir()?.join("scripts")) - .await - .context("failed to remove scripts directory")?; - - println!("scripts repo set to: {repo}"); - } - None => { - println!("current scripts repo: {}", config.scripts_repo); - } - } - - Ok(()) - } -} diff --git a/src/cli/commands/init.rs b/src/cli/commands/init.rs index e3aec28..f89bc44 100644 --- a/src/cli/commands/init.rs +++ b/src/cli/commands/init.rs @@ -1,30 +1,25 @@ -use std::{path::Path, str::FromStr}; - +use crate::cli::config::read_config; use anyhow::Context; use clap::Args; use colored::Colorize; use inquire::validator::Validation; - use pesde::{ - errors::ManifestReadError, names::PackageName, scripts::ScriptName, Project, DEFAULT_INDEX_NAME, + errors::ManifestReadError, + manifest::target::TargetKind, + names::PackageName, + source::{ + git_index::GitBasedSource, + pesde::{specifier::PesdeDependencySpecifier, PesdePackageSource}, + traits::PackageSource, + }, + Project, DEFAULT_INDEX_NAME, SCRIPTS_LINK_FOLDER, }; - -use crate::cli::{config::read_config, HOME_DIR}; -use fs_err::tokio as fs; +use semver::VersionReq; +use std::{collections::HashSet, str::FromStr}; #[derive(Debug, Args)] pub struct InitCommand {} -fn script_contents(path: &Path) -> String { - format!( - r#"local process = require("@lune/process") -local home_dir = if process.os == "windows" then process.env.userprofile else process.env.HOME - -require(home_dir .. {:?})"#, - format!("/{HOME_DIR}/scripts/{}", path.display()) - ) -} - impl InitCommand { pub async fn run(self, project: Project) -> anyhow::Result<()> { match project.read_manifest().await { @@ -39,7 +34,7 @@ impl InitCommand { let mut manifest = toml_edit::DocumentMut::new(); manifest["name"] = toml_edit::value( - inquire::Text::new("What is the name of the project?") + inquire::Text::new("what is the name of the project?") .with_validator(|name: &str| { Ok(match PackageName::from_str(name) { Ok(_) => Validation::Valid, @@ -51,20 +46,19 @@ impl InitCommand { ); manifest["version"] = toml_edit::value("0.1.0"); - let description = - inquire::Text::new("What is the description of the project? (leave empty for none)") - .prompt() - .unwrap(); + let description = inquire::Text::new("what is the description of the project?") + .with_help_message("a short 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 = inquire::Text::new("who are the authors of this project?") + .with_help_message("comma separated list. leave empty for none") + .prompt() + .unwrap(); let authors = authors .split(',') @@ -76,106 +70,117 @@ impl InitCommand { manifest["authors"] = toml_edit::value(authors); } - 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); - } + let repo = inquire::Text::new("what is the repository URL of this project?") + .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()), + Ok(match url::Url::parse(repo) { + Ok(_) => Validation::Valid, + Err(e) => Validation::Invalid(e.to_string().into()), + }) }) - }) - .prompt() - .unwrap(); + .with_help_message("leave empty for none") + .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(); + let license = inquire::Text::new("what is the license of this project?") + .with_initial_value("MIT") + .with_help_message("an SPDX license identifier. leave empty for none") + .prompt() + .unwrap(); 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!["roblox", "roblox_server", "lune", "luau"], + "what environment are you targeting for your package?", + TargetKind::VARIANTS.to_vec(), ) .prompt() .unwrap(); manifest["target"].or_insert(toml_edit::Item::Table(toml_edit::Table::new())) - ["environment"] = toml_edit::value(target_env); + ["environment"] = toml_edit::value(target_env.to_string()); - if target_env == "roblox" - || target_env == "roblox_server" - || inquire::Confirm::new(&format!( - "Would you like to setup a default {} script?", - ScriptName::RobloxSyncConfigGenerator - )) - .prompt() + let source = PesdePackageSource::new(read_config().await?.default_index); + + manifest["indices"].or_insert(toml_edit::Item::Table(toml_edit::Table::new())) + [DEFAULT_INDEX_NAME] = toml_edit::value(source.repo_url().to_bstring().to_string()); + + if target_env.is_roblox() + || inquire::prompt_confirmation( + "would you like to setup default Roblox compatibility scripts?", + ) .unwrap() { - let folder = project - .package_dir() - .join(concat!(".", env!("CARGO_PKG_NAME"))); - fs::create_dir_all(&folder) + PackageSource::refresh(&source, &project) .await - .context("failed to create scripts folder")?; + .context("failed to refresh package source")?; + let config = source + .config(&project) + .await + .context("failed to get source config")?; - fs::write( - folder.join(format!("{}.luau", ScriptName::RobloxSyncConfigGenerator)), - script_contents(Path::new(&format!( - "lune/rojo/{}.luau", - ScriptName::RobloxSyncConfigGenerator - ))), - ) - .await - .context("failed to write sync config generator script file")?; + if let Some(scripts_pkg_name) = config.scripts_package { + let (v_id, pkg_ref) = source + .resolve( + &PesdeDependencySpecifier { + name: scripts_pkg_name, + version: VersionReq::STAR, + index: None, + target: None, + }, + &project, + TargetKind::Lune, + &mut HashSet::new(), + ) + .await + .context("failed to resolve scripts package")? + .1 + .pop_last() + .context("scripts package not found")?; - #[cfg(feature = "wally-compat")] - fs::write( - folder.join(format!("{}.luau", ScriptName::SourcemapGenerator)), - script_contents(Path::new(&format!( - "lune/rojo/{}.luau", - ScriptName::SourcemapGenerator - ))), - ) - .await - .context("failed to write sourcemap generator script file")?; + let Some(scripts) = pkg_ref.target.scripts().filter(|s| !s.is_empty()) else { + anyhow::bail!("scripts package has no scripts. this is an issue with the index") + }; - let scripts = - manifest["scripts"].or_insert(toml_edit::Item::Table(toml_edit::Table::new())); + let scripts_field = &mut manifest["scripts"] + .or_insert(toml_edit::Item::Table(toml_edit::Table::new())); - scripts[&ScriptName::RobloxSyncConfigGenerator.to_string()] = - toml_edit::value(format!( - concat!(".", env!("CARGO_PKG_NAME"), "/{}.luau"), - ScriptName::RobloxSyncConfigGenerator - )); + for script_name in scripts.keys() { + scripts_field[script_name] = toml_edit::value(format!( + "{SCRIPTS_LINK_FOLDER}/scripts/{script_name}.luau" + )); + } - #[cfg(feature = "wally-compat")] - { - scripts[&ScriptName::SourcemapGenerator.to_string()] = toml_edit::value(format!( - concat!(".", env!("CARGO_PKG_NAME"), "/{}.luau"), - ScriptName::SourcemapGenerator - )); + let field = &mut manifest["dev_dependencies"] + .or_insert(toml_edit::Item::Table(toml_edit::Table::new()))["scripts"]; + field["name"] = toml_edit::value(pkg_ref.name.to_string()); + field["version"] = toml_edit::value(format!("^{}", v_id.version())); + field["target"] = toml_edit::value(v_id.target().to_string()); + } else { + println!( + "{}", + "configured index hasn't a configured scripts package".red() + ); + if !inquire::prompt_confirmation("initialize regardless?").unwrap() { + return Ok(()); + } } } - manifest["indices"].or_insert(toml_edit::Item::Table(toml_edit::Table::new())) - [DEFAULT_INDEX_NAME] = - toml_edit::value(read_config().await?.default_index.to_bstring().to_string()); - project.write_manifest(manifest.to_string()).await?; - println!("{}", "initialized project".green()); + println!( + "{}\n{}: run `install` to fully finish setup", + "initialized project".green(), + "tip".cyan().bold() + ); Ok(()) } } diff --git a/src/cli/commands/install.rs b/src/cli/commands/install.rs index 0f0568e..0adf539 100644 --- a/src/cli/commands/install.rs +++ b/src/cli/commands/install.rs @@ -1,6 +1,5 @@ use crate::cli::{ - bin_dir, files::make_executable, progress_bar, repos::update_scripts, run_on_workspace_members, - up_to_date_lockfile, + bin_dir, files::make_executable, progress_bar, run_on_workspace_members, up_to_date_lockfile, }; use anyhow::Context; use clap::Args; @@ -139,9 +138,6 @@ impl InstallCommand { } }; - let project_2 = project.clone(); - let update_scripts_handle = tokio::spawn(async move { update_scripts(&project_2).await }); - println!( "\n{}\n", format!("[now installing {} {}]", manifest.name, manifest.target) @@ -204,8 +200,6 @@ impl InstallCommand { .context("failed to build dependency graph")?; let graph = Arc::new(graph); - update_scripts_handle.await??; - let bin_folder = bin_dir().await?; let downloaded_graph = { diff --git a/src/cli/commands/publish.rs b/src/cli/commands/publish.rs index 918e578..298c02f 100644 --- a/src/cli/commands/publish.rs +++ b/src/cli/commands/publish.rs @@ -101,15 +101,21 @@ impl PublishCommand { } } + let canonical_package_dir = project + .package_dir() + .canonicalize() + .context("failed to canonicalize package directory")?; + let mut archive = tokio_tar::Builder::new( async_compression::tokio::write::GzipEncoder::with_quality(vec![], Level::Best), ); let mut display_build_files: Vec = vec![]; - let (lib_path, bin_path, target_kind) = ( + let (lib_path, bin_path, scripts, target_kind) = ( manifest.target.lib_path().cloned(), manifest.target.bin_path().cloned(), + manifest.target.scripts().cloned(), manifest.target.kind(), ); @@ -188,21 +194,24 @@ info: otherwise, the file was deemed unnecessary, if you don't understand why, p continue; }; - let export_path = relative_export_path - .to_path(project.package_dir()) + let export_path = relative_export_path.to_path(&canonical_package_dir); + + let contents = match fs::read_to_string(&export_path).await { + Ok(contents) => contents, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + anyhow::bail!("{name} does not exist"); + } + Err(e) if e.kind() == std::io::ErrorKind::IsADirectory => { + anyhow::bail!("{name} must point to a file"); + } + Err(e) => { + return Err(e).context(format!("failed to read {name}")); + } + }; + + let export_path = export_path .canonicalize() .context(format!("failed to canonicalize {name}"))?; - if !export_path.exists() { - anyhow::bail!("{name} points to non-existent file"); - } - - if !export_path.is_file() { - anyhow::bail!("{name} must point to a file"); - } - - let contents = fs::read_to_string(&export_path) - .await - .context(format!("failed to read {name}"))?; if let Err(err) = full_moon::parse(&contents).map_err(|errs| { errs.into_iter() @@ -223,7 +232,12 @@ info: otherwise, the file was deemed unnecessary, if you don't understand why, p _ => anyhow::bail!("{name} must be within project directory"), }; - if paths.insert(PathBuf::from(relative_export_path.as_str())) { + if paths.insert( + export_path + .strip_prefix(&canonical_package_dir) + .unwrap() + .to_path_buf(), + ) { println!( "{}: {name} was not included, adding {relative_export_path}", "warn".yellow().bold() @@ -270,6 +284,50 @@ info: otherwise, the file was deemed unnecessary, if you don't understand why, p } } + if let Some(scripts) = scripts { + for (name, path) in scripts { + let script_path = path.to_path(&canonical_package_dir); + + let contents = match fs::read_to_string(&script_path).await { + Ok(contents) => contents, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + anyhow::bail!("script {name} does not exist"); + } + Err(e) if e.kind() == std::io::ErrorKind::IsADirectory => { + anyhow::bail!("script {name} must point to a file"); + } + Err(e) => { + return Err(e).context(format!("failed to read script {name}")); + } + }; + + let script_path = script_path + .canonicalize() + .context(format!("failed to canonicalize script {name}"))?; + + if let Err(err) = full_moon::parse(&contents).map_err(|errs| { + errs.into_iter() + .map(|err| err.to_string()) + .collect::>() + .join(", ") + }) { + anyhow::bail!("script {name} is not a valid Luau file: {err}"); + } + + if paths.insert( + script_path + .strip_prefix(&canonical_package_dir) + .unwrap() + .to_path_buf(), + ) { + println!( + "{}: script {name} was not included, adding {path}", + "warn".yellow().bold() + ); + } + } + } + for relative_path in &paths { let path = project.package_dir().join(relative_path); diff --git a/src/cli/commands/run.rs b/src/cli/commands/run.rs index 68ddc52..615958d 100644 --- a/src/cli/commands/run.rs +++ b/src/cli/commands/run.rs @@ -1,4 +1,4 @@ -use crate::cli::{repos::update_scripts, up_to_date_lockfile}; +use crate::cli::up_to_date_lockfile; use anyhow::Context; use clap::Args; use futures::{StreamExt, TryStreamExt}; @@ -27,34 +27,29 @@ pub struct RunCommand { impl RunCommand { pub async fn run(self, project: Project) -> anyhow::Result<()> { let run = |root: PathBuf, file_path: PathBuf| { - let fut = update_scripts(&project); - async move { - fut.await.expect("failed to update scripts"); - - let mut caller = tempfile::NamedTempFile::new().expect("failed to create tempfile"); - caller - .write_all( - generate_bin_linking_module( - root, - &format!("{:?}", file_path.to_string_lossy()), - ) - .as_bytes(), + let mut caller = tempfile::NamedTempFile::new().expect("failed to create tempfile"); + caller + .write_all( + generate_bin_linking_module( + root, + &format!("{:?}", file_path.to_string_lossy()), ) - .expect("failed to write to tempfile"); + .as_bytes(), + ) + .expect("failed to write to tempfile"); - let status = Command::new("lune") - .arg("run") - .arg(caller.path()) - .arg("--") - .args(&self.args) - .current_dir(current_dir().expect("failed to get current directory")) - .status() - .expect("failed to run script"); + let status = Command::new("lune") + .arg("run") + .arg(caller.path()) + .arg("--") + .args(&self.args) + .current_dir(current_dir().expect("failed to get current directory")) + .status() + .expect("failed to run script"); - drop(caller); + drop(caller); - std::process::exit(status.code().unwrap_or(1)) - } + std::process::exit(status.code().unwrap_or(1)) }; let Some(package_or_script) = self.package_or_script else { @@ -62,8 +57,7 @@ impl RunCommand { run( project.package_dir().to_owned(), script_path.to_path(project.package_dir()), - ) - .await; + ); return Ok(()); } @@ -105,7 +99,7 @@ impl RunCommand { let path = bin_path.to_path(&container_folder); - run(path.clone(), path).await; + run(path.clone(), path); return Ok(()); } } @@ -115,8 +109,7 @@ impl RunCommand { run( project.package_dir().to_path_buf(), script_path.to_path(project.package_dir()), - ) - .await; + ); return Ok(()); } }; @@ -177,7 +170,7 @@ impl RunCommand { project.package_dir().to_path_buf() }; - run(root, path).await; + run(root, path); Ok(()) } diff --git a/src/cli/commands/update.rs b/src/cli/commands/update.rs index 863332b..d079594 100644 --- a/src/cli/commands/update.rs +++ b/src/cli/commands/update.rs @@ -1,4 +1,4 @@ -use crate::cli::{progress_bar, repos::update_scripts, run_on_workspace_members}; +use crate::cli::{progress_bar, run_on_workspace_members}; use anyhow::Context; use clap::Args; use colored::Colorize; @@ -37,8 +37,6 @@ impl UpdateCommand { .context("failed to build dependency graph")?; let graph = Arc::new(graph); - update_scripts(&project).await?; - project .write_lockfile(Lockfile { name: manifest.name, diff --git a/src/cli/config.rs b/src/cli/config.rs index add7097..dce9ca2 100644 --- a/src/cli/config.rs +++ b/src/cli/config.rs @@ -4,17 +4,13 @@ use fs_err::tokio as fs; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] pub struct CliConfig { #[serde( serialize_with = "crate::util::serialize_gix_url", deserialize_with = "crate::util::deserialize_gix_url" )] pub default_index: gix::Url, - #[serde( - serialize_with = "crate::util::serialize_gix_url", - deserialize_with = "crate::util::deserialize_gix_url" - )] - pub scripts_repo: gix::Url, pub tokens: Tokens, @@ -26,7 +22,6 @@ impl Default for CliConfig { fn default() -> Self { Self { default_index: "https://github.com/pesde-pkg/index".try_into().unwrap(), - scripts_repo: "https://github.com/pesde-pkg/scripts".try_into().unwrap(), tokens: Tokens(Default::default()), diff --git a/src/cli/mod.rs b/src/cli/mod.rs index fc2bdba..beeaa7c 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -24,7 +24,6 @@ pub mod auth; pub mod commands; pub mod config; pub mod files; -pub mod repos; #[cfg(feature = "version-management")] pub mod version; diff --git a/src/cli/repos.rs b/src/cli/repos.rs deleted file mode 100644 index 695c4e4..0000000 --- a/src/cli/repos.rs +++ /dev/null @@ -1,143 +0,0 @@ -use crate::{ - cli::{config::read_config, home_dir}, - util::authenticate_conn, -}; -use anyhow::Context; -use fs_err::tokio as fs; -use gix::remote::{fetch::Shallow, Direction}; -use pesde::Project; -use std::{path::Path, sync::atomic::AtomicBool}; -use tokio::{runtime::Handle, task::spawn_blocking}; - -async fn update_repo>( - name: &str, - path: P, - url: gix::Url, - project: &Project, -) -> anyhow::Result<()> { - let path = path.as_ref(); - let should_update = path.exists(); - - let (repo, oid) = if should_update { - let repo = gix::open(path).context(format!("failed to open {name} repository"))?; - - let remote = repo - .find_default_remote(Direction::Fetch) - .context(format!("missing default remote of {name} repository"))? - .context(format!( - "failed to find default remote of {name} repository" - ))?; - - let mut connection = remote.connect(Direction::Fetch).context(format!( - "failed to connect to default remote of {name} repository" - ))?; - - authenticate_conn(&mut connection, project.auth_config()); - - let results = connection - .prepare_fetch(gix::progress::Discard, Default::default()) - .context(format!("failed to prepare {name} repository fetch"))? - .with_shallow(Shallow::Deepen(1)) - .receive(gix::progress::Discard, &false.into()) - .context(format!("failed to receive new {name} repository contents"))?; - - let remote_ref = results - .ref_map - .remote_refs - .first() - .context(format!("failed to get remote refs of {name} repository"))?; - - let unpacked = remote_ref.unpack(); - let oid = unpacked - .1 - .or(unpacked.2) - .context("couldn't find oid in remote ref")?; - - (repo, gix::ObjectId::from(oid)) - } else { - fs::create_dir_all(path) - .await - .context(format!("failed to create {name} directory"))?; - - let repo = gix::prepare_clone(url, path) - .context(format!("failed to prepare {name} repository clone"))? - .with_shallow(Shallow::Deepen(1)) - .fetch_only(gix::progress::Discard, &false.into()) - .context(format!("failed to fetch and checkout {name} repository"))? - .0; - - let oid = { - let mut head = repo - .head() - .context(format!("failed to get {name} repository head"))?; - let obj = head - .peel_to_object_in_place() - .context(format!("failed to peel {name} repository head to object"))?; - - obj.id - }; - - (repo, oid) - }; - - let tree = repo - .find_object(oid) - .context(format!("failed to find {name} repository tree"))? - .peel_to_tree() - .context(format!("failed to peel {name} repository object to tree"))?; - - let mut index = gix::index::File::from_state( - gix::index::State::from_tree(&tree.id, &repo.objects, Default::default()).context( - format!("failed to create index state from {name} repository tree"), - )?, - repo.index_path(), - ); - - let opts = gix::worktree::state::checkout::Options { - overwrite_existing: true, - destination_is_initially_empty: !should_update, - ..Default::default() - }; - - gix::worktree::state::checkout( - &mut index, - repo.work_dir().context(format!("{name} repo is bare"))?, - repo.objects - .clone() - .into_arc() - .context("failed to clone objects")?, - &gix::progress::Discard, - &gix::progress::Discard, - &false.into(), - opts, - ) - .context(format!("failed to checkout {name} repository"))?; - - index - .write(gix::index::write::Options::default()) - .context("failed to write index") -} - -static SCRIPTS_UPDATED: AtomicBool = AtomicBool::new(false); - -pub async fn update_scripts(project: &Project) -> anyhow::Result<()> { - if SCRIPTS_UPDATED.swap(true, std::sync::atomic::Ordering::Relaxed) { - return Ok(()); - } - - let home_dir = home_dir()?; - let config = read_config().await?; - - let project = project.clone(); - spawn_blocking(move || { - Handle::current().block_on(update_repo( - "scripts", - home_dir.join("scripts"), - config.scripts_repo, - &project, - )) - }) - .await??; - - Ok(()) -} diff --git a/src/download_and_link.rs b/src/download_and_link.rs index e3a33c2..e0ff57e 100644 --- a/src/download_and_link.rs +++ b/src/download_and_link.rs @@ -148,6 +148,7 @@ pub mod errors { /// An error that can occur when downloading and linking dependencies #[derive(Debug, Error)] + #[non_exhaustive] pub enum DownloadAndLinkError { /// An error occurred while downloading the graph #[error("error downloading graph")] diff --git a/src/lib.rs b/src/lib.rs index c1812cb..99ad23e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,6 +50,8 @@ pub const DEFAULT_INDEX_NAME: &str = "default"; /// The name of the packages container pub const PACKAGES_CONTAINER_NAME: &str = ".pesde"; pub(crate) const LINK_LIB_NO_FILE_FOUND: &str = "____pesde_no_export_file_found"; +/// The folder in which scripts are linked +pub const SCRIPTS_LINK_FOLDER: &str = ".pesde"; /// Struct containing the authentication configuration #[derive(Debug, Default, Clone)] diff --git a/src/linking/generator.rs b/src/linking/generator.rs index ed2add3..ea7b9b6 100644 --- a/src/linking/generator.rs +++ b/src/linking/generator.rs @@ -199,12 +199,30 @@ pub fn get_bin_require_path( luau_style_path(&path) } +/// Generate a linking module for a script +pub fn generate_script_linking_module(require_path: &str) -> String { + format!(r#"return require({require_path})"#) +} + +/// Get the require path for a script +pub fn get_script_require_path( + base_dir: &Path, + script_file: &RelativePathBuf, + destination_dir: &Path, +) -> String { + let path = pathdiff::diff_paths(destination_dir, base_dir).unwrap(); + let path = script_file.to_path(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)] + #[non_exhaustive] pub enum GetLibRequirePath { /// The path for the RobloxPlaceKind could not be found #[error("could not find the path for the RobloxPlaceKind {0}")] diff --git a/src/linking/mod.rs b/src/linking/mod.rs index a882aae..b1de1fb 100644 --- a/src/linking/mod.rs +++ b/src/linking/mod.rs @@ -1,6 +1,6 @@ use crate::{ linking::generator::get_file_types, - lockfile::DownloadedGraph, + lockfile::{DownloadedDependencyGraphNode, DownloadedGraph}, manifest::Manifest, names::PackageNames, scripts::{execute_script, ScriptName}, @@ -9,7 +9,7 @@ use crate::{ traits::PackageRef, version_id::VersionId, }, - Project, LINK_LIB_NO_FILE_FOUND, PACKAGES_CONTAINER_NAME, + Project, LINK_LIB_NO_FILE_FOUND, PACKAGES_CONTAINER_NAME, SCRIPTS_LINK_FOLDER, }; use fs_err::tokio as fs; use futures::future::try_join_all; @@ -157,14 +157,93 @@ impl Project { self.link(graph, &manifest, &Arc::new(package_types)).await } + #[allow(clippy::too_many_arguments)] + async fn link_files( + &self, + base_folder: &Path, + container_folder: &Path, + root_container_folder: &Path, + relative_container_folder: &Path, + node: &DownloadedDependencyGraphNode, + name: &PackageNames, + version_id: &VersionId, + alias: &str, + package_types: &HashMap<&PackageNames, HashMap<&VersionId, Vec>>, + manifest: &Manifest, + ) -> Result<(), errors::LinkingError> { + static NO_TYPES: Vec = Vec::new(); + + if let Some(lib_file) = node.target.lib_path() { + let lib_module = generator::generate_lib_linking_module( + &generator::get_lib_require_path( + &node.target.kind(), + base_folder, + lib_file, + container_folder, + node.node.pkg_ref.use_new_structure(), + root_container_folder, + relative_container_folder, + manifest, + )?, + package_types + .get(name) + .and_then(|v| v.get(version_id)) + .unwrap_or(&NO_TYPES), + ); + + write_cas( + base_folder.join(format!("{alias}.luau")), + self.cas_dir(), + &lib_module, + ) + .await?; + } + + if let Some(bin_file) = node.target.bin_path() { + let bin_module = generator::generate_bin_linking_module( + container_folder, + &generator::get_bin_require_path(base_folder, bin_file, container_folder), + ); + + write_cas( + base_folder.join(format!("{alias}.bin.luau")), + self.cas_dir(), + &bin_module, + ) + .await?; + } + + if let Some(scripts) = node.target.scripts() { + let scripts_base = + create_and_canonicalize(self.package_dir().join(SCRIPTS_LINK_FOLDER).join(alias)) + .await?; + + for (script_name, script_path) in scripts { + let script_module = + generator::generate_script_linking_module(&generator::get_script_require_path( + &scripts_base, + script_path, + container_folder, + )); + + write_cas( + scripts_base.join(format!("{script_name}.luau")), + self.cas_dir(), + &script_module, + ) + .await?; + } + } + + Ok(()) + } + async fn link( &self, graph: &DownloadedGraph, manifest: &Arc, package_types: &Arc>>>, ) -> Result<(), errors::LinkingError> { - static NO_TYPES: Vec = Vec::new(); - try_join_all(graph.iter().flat_map(|(name, versions)| { versions.iter().map(|(version_id, node)| { let name = name.clone(); @@ -186,46 +265,20 @@ impl Project { version_id.version(), ); - if let Some((alias, _, _)) = &node.node.direct.as_ref() { - if let Some(lib_file) = node.target.lib_path() { - write_cas( - base_folder.join(format!("{alias}.luau")), - self.cas_dir(), - &generator::generate_lib_linking_module( - &generator::get_lib_require_path( - &node.target.kind(), - &base_folder, - lib_file, - &container_folder, - node.node.pkg_ref.use_new_structure(), - &base_folder, - container_folder.strip_prefix(&base_folder).unwrap(), - &manifest, - )?, - package_types - .get(&name) - .and_then(|v| v.get(version_id)) - .unwrap_or(&NO_TYPES), - ), - ) - .await?; - }; - - if let Some(bin_file) = node.target.bin_path() { - write_cas( - base_folder.join(format!("{alias}.bin.luau")), - self.cas_dir(), - &generator::generate_bin_linking_module( - &container_folder, - &generator::get_bin_require_path( - &base_folder, - bin_file, - &container_folder, - ), - ), - ) - .await?; - } + if let Some((alias, _, _)) = &node.node.direct { + self.link_files( + &base_folder, + &container_folder, + &base_folder, + container_folder.strip_prefix(&base_folder).unwrap(), + node, + &name, + version_id, + alias, + &package_types, + &manifest, + ) + .await?; } (container_folder, base_folder) @@ -244,10 +297,6 @@ impl Project { )); }; - let Some(lib_file) = dependency_node.target.lib_path() else { - continue; - }; - let base_folder = create_and_canonicalize( self.package_dir().join( version_id @@ -272,25 +321,17 @@ impl Project { ) .await?; - write_cas( - linker_folder.join(format!("{dependency_alias}.luau")), - self.cas_dir(), - &generator::generate_lib_linking_module( - &generator::get_lib_require_path( - &dependency_node.target.kind(), - &linker_folder, - lib_file, - &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)) - .unwrap_or(&NO_TYPES), - ), + self.link_files( + &linker_folder, + &container_folder, + &node_packages_folder, + container_folder.strip_prefix(&base_folder).unwrap(), + dependency_node, + dependency_name, + dependency_version_id, + dependency_alias, + &package_types, + &manifest, ) .await?; } diff --git a/src/manifest/target.rs b/src/manifest/target.rs index 803c8bd..850527c 100644 --- a/src/manifest/target.rs +++ b/src/manifest/target.rs @@ -2,7 +2,7 @@ use relative_path::RelativePathBuf; use serde::{Deserialize, Serialize}; use serde_with::{DeserializeFromStr, SerializeDisplay}; use std::{ - collections::BTreeSet, + collections::{BTreeMap, BTreeSet}, fmt::{Display, Formatter}, str::FromStr, }; @@ -68,6 +68,11 @@ impl TargetKind { format!("{dependency}_packages") } + + /// Returns whether this target is a Roblox target + pub fn is_roblox(&self) -> bool { + matches!(self, TargetKind::Roblox | TargetKind::RobloxServer) + } } /// A target of a package @@ -77,7 +82,7 @@ pub enum Target { /// A Roblox target Roblox { /// The path to the lib export file - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] lib: Option, /// The files to include in the sync tool's config #[serde(default)] @@ -86,7 +91,7 @@ pub enum Target { /// A Roblox server target RobloxServer { /// The path to the lib export file - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] lib: Option, /// The files to include in the sync tool's config #[serde(default)] @@ -95,19 +100,22 @@ pub enum Target { /// A Lune target Lune { /// The path to the lib export file - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] lib: Option, /// The path to the bin export file - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] bin: Option, + /// The exported scripts + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + scripts: BTreeMap, }, /// A Luau target Luau { /// The path to the lib export file - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] lib: Option, /// The path to the bin export file - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] bin: Option, }, } @@ -151,6 +159,14 @@ impl Target { _ => None, } } + + /// Returns the scripts exported by this target + pub fn scripts(&self) -> Option<&BTreeMap> { + match self { + Target::Lune { scripts, .. } => Some(scripts), + _ => None, + } + } } impl Display for Target { diff --git a/src/source/pesde/mod.rs b/src/source/pesde/mod.rs index e44b8cf..a0b8c65 100644 --- a/src/source/pesde/mod.rs +++ b/src/source/pesde/mod.rs @@ -17,7 +17,7 @@ use crate::{ target::{Target, TargetKind}, DependencyType, }, - names::PackageNames, + names::{PackageName, PackageNames}, source::{ fs::{store_in_cas, FSEntry, PackageFS}, git_index::{read_file, root_tree, GitBasedSource}, @@ -316,6 +316,9 @@ pub struct IndexConfig { /// The maximum size of an archive in bytes #[serde(default = "default_archive_size")] pub max_archive_size: usize, + /// The package to use for default script implementations + #[serde(default)] + pub scripts_package: Option, } impl IndexConfig { diff --git a/src/source/workspace/specifier.rs b/src/source/workspace/specifier.rs index 995348f..6025c61 100644 --- a/src/source/workspace/specifier.rs +++ b/src/source/workspace/specifier.rs @@ -108,6 +108,7 @@ pub mod errors { /// Errors that can occur when parsing a version type #[derive(Debug, Error)] + #[non_exhaustive] pub enum VersionTypeFromStr { /// The version type is invalid #[error("invalid version type {0}")] @@ -116,6 +117,7 @@ pub mod errors { /// Errors that can occur when parsing a version type or requirement #[derive(Debug, Error)] + #[non_exhaustive] pub enum VersionTypeOrReqFromStr { /// The version requirement is invalid #[error("invalid version requirement {0}")]