From f1ce6283d8711a891db79caec0ba7cd78d044ec6 Mon Sep 17 00:00:00 2001 From: daimond113 <72147841+daimond113@users.noreply.github.com> Date: Tue, 3 Sep 2024 16:01:48 +0200 Subject: [PATCH] feat: implement workspace/monorepo support --- Cargo.lock | 11 +- Cargo.toml | 17 ++- registry/src/endpoints/publish_version.rs | 11 +- registry/src/main.rs | 4 +- src/cli/commands/add.rs | 1 + src/cli/commands/execute.rs | 2 +- src/cli/commands/init.rs | 4 +- src/cli/commands/install.rs | 65 ++++++-- src/cli/commands/outdated.rs | 6 +- src/cli/commands/patch_commit.rs | 2 +- src/cli/commands/publish.rs | 56 ++++++- src/cli/commands/run.rs | 39 +++-- src/cli/files.rs | 1 - src/download.rs | 9 +- src/lib.rs | 99 +++++++++++-- src/linking/generator.rs | 4 +- src/linking/mod.rs | 123 ++++++++------- src/lockfile.rs | 15 +- src/main.rs | 96 +++++++++--- src/manifest/mod.rs | 3 + src/patches.rs | 16 +- src/resolver.rs | 8 + src/scripts.rs | 2 +- src/source/fs.rs | 94 +++++++++--- src/source/git/mod.rs | 36 +++-- src/source/mod.rs | 46 +++++- src/source/pesde/mod.rs | 6 +- src/source/refs.rs | 6 + src/source/specifiers.rs | 3 + src/source/wally/compat_util.rs | 2 +- src/source/wally/mod.rs | 4 +- src/source/workspace/mod.rs | 173 ++++++++++++++++++++++ src/source/workspace/pkg_ref.rs | 40 +++++ src/source/workspace/specifier.rs | 78 ++++++++++ 34 files changed, 880 insertions(+), 202 deletions(-) create mode 100644 src/source/workspace/mod.rs create mode 100644 src/source/workspace/pkg_ref.rs create mode 100644 src/source/workspace/specifier.rs diff --git a/Cargo.lock b/Cargo.lock index 649a4a8..038190c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2335,6 +2335,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "governor" version = "0.6.3" @@ -2855,9 +2861,9 @@ dependencies = [ [[package]] name = "keyring" -version = "3.2.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b9af47ded4df3067484d7d45758ca2b36bd083bf6d024c2952bbd8af1cdaa4" +checksum = "030a9b84bb2a2f3673d4c8b8236091ed5d8f6b66a56d8085471d8abd5f3c6a80" dependencies = [ "byteorder", "dbus-secret-service", @@ -3506,6 +3512,7 @@ dependencies = [ "full_moon", "git2", "gix", + "glob", "indicatif", "indicatif-log-bridge", "inquire", diff --git a/Cargo.toml b/Cargo.toml index 4ee0141..a13a704 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,14 +43,14 @@ required-features = ["bin"] uninlined_format_args = "warn" [dependencies] -serde = { version = "1.0.204", features = ["derive"] } +serde = { version = "1.0.209", features = ["derive"] } toml = "0.8.19" serde_with = "3.9.0" gix = { version = "0.66.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"] } +reqwest = { version = "0.12.7", default-features = false, features = ["rustls-tls", "blocking"] } tar = "0.4.41" -flate2 = "1.0.31" +flate2 = "1.0.33" pathdiff = "0.2.1" relative-path = { version = "1.9.3", features = ["serde"] } log = "0.4.22" @@ -63,23 +63,24 @@ url = { version = "2.5.2", features = ["serde"] } chrono = { version = "0.4.38", features = ["serde"] } sha2 = "0.10.8" tempfile = "3.12.0" +glob = "0.3.1" # TODO: remove this when gitoxide adds support for: committing, pushing, adding git2 = { version = "0.19.0", optional = true } -zip = { version = "2.1.6", optional = true } -serde_json = { version = "1.0.122", optional = true } +zip = { version = "2.2.0", optional = true } +serde_json = { version = "1.0.127", optional = true } anyhow = { version = "1.0.86", optional = true } open = { version = "5.3.0", optional = true } -keyring = { version = "3.0.5", features = ["crypto-rust", "windows-native", "apple-native", "sync-secret-service"], optional = true } +keyring = { version = "3.2.1", features = ["crypto-rust", "windows-native", "apple-native", "sync-secret-service"], optional = true } colored = { version = "2.1.0", optional = true } toml_edit = { version = "0.22.20", optional = true } -clap = { version = "4.5.13", features = ["derive"], optional = true } +clap = { version = "4.5.16", features = ["derive"], optional = true } dirs = { version = "5.0.1", optional = true } pretty_env_logger = { version = "0.5.0", optional = true } indicatif = { version = "0.17.8", optional = true } -indicatif-log-bridge = { version = "0.2.2", optional = true } +indicatif-log-bridge = { version = "0.2.3", optional = true } inquire = { version = "0.7.5", optional = true } [target.'cfg(target_os = "windows")'.dependencies] diff --git a/registry/src/endpoints/publish_version.rs b/registry/src/endpoints/publish_version.rs index 337a219..365feed 100644 --- a/registry/src/endpoints/publish_version.rs +++ b/registry/src/endpoints/publish_version.rs @@ -298,13 +298,6 @@ pub async fn publish_package( { return Err(Error::InvalidArchive); } - - let (dep_scope, dep_name) = specifier.name.as_str(); - match source.read_file([dep_scope, dep_name], &app_state.project, None) { - Ok(Some(_)) => {} - Ok(None) => return Err(Error::InvalidArchive), - Err(e) => return Err(e.into()), - } } DependencySpecifiers::Wally(specifier) => { if !config.wally_allowed { @@ -325,6 +318,10 @@ pub async fn publish_package( return Err(Error::InvalidArchive); } } + DependencySpecifiers::Workspace(_) => { + // workspace specifiers are to be transformed into Pesde specifiers by the sender + return Err(Error::InvalidArchive); + } } } diff --git a/registry/src/main.rs b/registry/src/main.rs index 26375ef..c48fe07 100644 --- a/registry/src/main.rs +++ b/registry/src/main.rs @@ -1,5 +1,3 @@ -use std::{env::current_dir, fs::create_dir_all, sync::Mutex}; - use actix_cors::Cors; use actix_governor::{Governor, GovernorConfigBuilder}; use actix_web::{ @@ -9,6 +7,7 @@ use actix_web::{ }; use log::info; use rusty_s3::{Bucket, Credentials, UrlStyle}; +use std::{env::current_dir, fs::create_dir_all, path::PathBuf, sync::Mutex}; use pesde::{ source::{pesde::PesdePackageSource, traits::PackageSource}, @@ -78,6 +77,7 @@ async fn run(with_sentry: bool) -> std::io::Result<()> { let project = Project::new( &cwd, + None::, data_dir.join("project"), &cwd, AuthConfig::new().with_git_credentials(Some(gix::sec::identity::Account { diff --git a/src/cli/commands/add.rs b/src/cli/commands/add.rs index 72c9937..3b7e85e 100644 --- a/src/cli/commands/add.rs +++ b/src/cli/commands/add.rs @@ -197,6 +197,7 @@ impl AddCommand { println!("added git {}#{} to {}", spec.repo, spec.rev, dependency_key); } + DependencySpecifiers::Workspace(_) => todo!(), } project diff --git a/src/cli/commands/execute.rs b/src/cli/commands/execute.rs index 4787154..8191533 100644 --- a/src/cli/commands/execute.rs +++ b/src/cli/commands/execute.rs @@ -77,7 +77,7 @@ impl ExecuteCommand { .arg(bin_path.to_path(tempdir.path())) .arg("--") .args(&self.args) - .current_dir(project.path()) + .current_dir(project.package_dir()) .status() .context("failed to run script")?; diff --git a/src/cli/commands/init.rs b/src/cli/commands/init.rs index 239c477..36fe9a2 100644 --- a/src/cli/commands/init.rs +++ b/src/cli/commands/init.rs @@ -133,7 +133,9 @@ impl InitCommand { .prompt() .unwrap() { - let folder = project.path().join(concat!(".", env!("CARGO_PKG_NAME"))); + let folder = project + .package_dir() + .join(concat!(".", env!("CARGO_PKG_NAME"))); std::fs::create_dir_all(&folder).context("failed to create scripts folder")?; std::fs::write( diff --git a/src/cli/commands/install.rs b/src/cli/commands/install.rs index 167cc9d..9fbe61c 100644 --- a/src/cli/commands/install.rs +++ b/src/cli/commands/install.rs @@ -1,17 +1,15 @@ +use crate::cli::{bin_dir, files::make_executable, IsUpToDate}; +use anyhow::Context; +use clap::Args; +use indicatif::MultiProgress; +use pesde::{lockfile::Lockfile, manifest::target::TargetKind, Project, MANIFEST_FILE_NAME}; +use relative_path::RelativePathBuf; use std::{ collections::{BTreeSet, HashSet}, sync::Arc, time::Duration, }; -use anyhow::Context; -use clap::Args; -use indicatif::MultiProgress; - -use pesde::{lockfile::Lockfile, manifest::target::TargetKind, Project, MANIFEST_FILE_NAME}; - -use crate::cli::{bin_dir, files::make_executable, IsUpToDate}; - #[derive(Debug, Args)] pub struct InstallCommand { /// The amount of threads to use for downloading @@ -44,7 +42,7 @@ fn bin_link_file(alias: &str) -> String { let prefix = String::new(); #[cfg(unix)] let prefix = "#!/usr/bin/env -S lune run\n"; - + // TODO: reimplement workspace support in this format!( r#"{prefix}local process = require("@lune/process") local fs = require("@lune/fs") @@ -113,7 +111,7 @@ impl InstallCommand { if deleted_folders.insert(folder.to_string()) { log::debug!("deleting the {folder} folder"); - if let Some(e) = std::fs::remove_dir_all(project.path().join(&folder)) + if let Some(e) = std::fs::remove_dir_all(project.package_dir().join(&folder)) .err() .filter(|e| e.kind() != std::io::ErrorKind::NotFound) { @@ -150,7 +148,7 @@ impl InstallCommand { "{msg} {bar:40.208/166} {pos}/{len} {percent}% {elapsed_precise}", )?, ) - .with_message("downloading dependencies"), + .with_message(format!("downloading dependencies of {}", manifest.name)), ); bar.enable_steady_tick(Duration::from_millis(100)); @@ -172,7 +170,10 @@ impl InstallCommand { } } - bar.finish_with_message("finished downloading dependencies"); + bar.finish_with_message(format!( + "finished downloading dependencies of {}", + manifest.name + )); let downloaded_graph = Arc::into_inner(downloaded_graph) .unwrap() @@ -229,6 +230,46 @@ impl InstallCommand { version: manifest.version, target: manifest.target.kind(), overrides: manifest.overrides, + workspace: match project.workspace_dir() { + Some(_) => { + // this might seem counterintuitive, but remember that the workspace + // is the package_dir when the user isn't in a member package + Default::default() + } + None => project + .workspace_members(project.package_dir()) + .context("failed to get workspace members")? + .into_iter() + .map(|(path, manifest)| { + ( + manifest.name, + RelativePathBuf::from_path( + path.strip_prefix(project.package_dir()).unwrap(), + ) + .unwrap(), + ) + }) + .map(|(name, path)| { + InstallCommand { + threads: self.threads, + unlocked: self.unlocked, + } + .run( + Project::new( + path.to_path(project.package_dir()), + Some(project.package_dir()), + project.data_dir(), + project.cas_dir(), + project.auth_config().clone(), + ), + multi.clone(), + reqwest.clone(), + ) + .map(|_| (name, path)) + }) + .collect::>() + .context("failed to install workspace member's dependencies")?, + }, graph: downloaded_graph, }) diff --git a/src/cli/commands/outdated.rs b/src/cli/commands/outdated.rs index 87f885c..d9dde2e 100644 --- a/src/cli/commands/outdated.rs +++ b/src/cli/commands/outdated.rs @@ -35,7 +35,10 @@ impl OutdatedCommand { continue; }; - if matches!(specifier, DependencySpecifiers::Git(_)) { + if matches!( + specifier, + DependencySpecifiers::Git(_) | DependencySpecifiers::Workspace(_) + ) { continue; } @@ -55,6 +58,7 @@ impl OutdatedCommand { spec.version = VersionReq::STAR; } DependencySpecifiers::Git(_) => {} + DependencySpecifiers::Workspace(_) => {} }; } diff --git a/src/cli/commands/patch_commit.rs b/src/cli/commands/patch_commit.rs index ab2a4fe..83e1fc0 100644 --- a/src/cli/commands/patch_commit.rs +++ b/src/cli/commands/patch_commit.rs @@ -55,7 +55,7 @@ impl PatchCommitCommand { let patch = create_patch(&self.directory).context("failed to create patch")?; std::fs::remove_dir_all(self.directory).context("failed to remove patch directory")?; - let patches_dir = project.path().join("patches"); + let patches_dir = project.package_dir().join("patches"); std::fs::create_dir_all(&patches_dir).context("failed to create patches directory")?; let patch_file_name = format!("{}-{}.patch", name.escaped(), version_id.escaped(),); diff --git a/src/cli/commands/publish.rs b/src/cli/commands/publish.rs index 05d42f6..77e4525 100644 --- a/src/cli/commands/publish.rs +++ b/src/cli/commands/publish.rs @@ -7,13 +7,17 @@ use anyhow::Context; use clap::Args; use colored::Colorize; use reqwest::StatusCode; +use semver::VersionReq; use tempfile::tempfile; use pesde::{ manifest::target::Target, scripts::ScriptName, source::{ - pesde::PesdePackageSource, specifiers::DependencySpecifiers, traits::PackageSource, + pesde::{specifier::PesdeDependencySpecifier, PesdePackageSource}, + specifiers::DependencySpecifiers, + traits::PackageSource, + workspace::{specifier::VersionType, WorkspacePackageSource}, IGNORED_DIRS, IGNORED_FILES, }, Project, DEFAULT_INDEX_NAME, MANIFEST_FILE_NAME, @@ -52,9 +56,10 @@ impl PublishCommand { #[cfg(feature = "roblox")] let mut display_build_files: Vec = vec![]; - let (lib_path, bin_path) = ( + let (lib_path, bin_path, target_kind) = ( manifest.target.lib_path().cloned(), manifest.target.bin_path().cloned(), + manifest.target.kind(), ); #[cfg(feature = "roblox")] @@ -124,7 +129,7 @@ impl PublishCommand { for (name, path) in [("lib path", lib_path), ("bin path", bin_path)] { let Some(export_path) = path else { continue }; - let export_path = export_path.to_path(project.path()); + let export_path = export_path.to_path(project.package_dir()); if !export_path.exists() { anyhow::bail!("{name} points to non-existent file"); } @@ -146,7 +151,7 @@ impl PublishCommand { } let first_part = export_path - .strip_prefix(project.path()) + .strip_prefix(project.package_dir()) .context(format!("{name} not within project directory"))? .components() .next() @@ -177,7 +182,7 @@ impl PublishCommand { } for included_name in &manifest.includes { - let included_path = project.path().join(included_name); + let included_path = project.package_dir().join(included_name); if !included_path.exists() { anyhow::bail!("included file {included_name} does not exist"); @@ -216,7 +221,7 @@ impl PublishCommand { continue; } - let build_file_path = project.path().join(build_file); + let build_file_path = project.package_dir().join(build_file); if !build_file_path.exists() { anyhow::bail!("build file {build_file} does not exist"); @@ -281,6 +286,45 @@ impl PublishCommand { DependencySpecifiers::Git(_) => { has_git = true; } + DependencySpecifiers::Workspace(spec) => { + let pkg_ref = WorkspacePackageSource + .resolve(spec, &project, target_kind) + .context("failed to resolve workspace package")? + .1 + .pop_last() + .context("no versions found for workspace package")? + .1; + + let manifest = pkg_ref + .path + .to_path( + project + .workspace_dir() + .context("failed to get workspace directory")?, + ) + .join(MANIFEST_FILE_NAME); + let manifest = std::fs::read_to_string(&manifest) + .context("failed to read workspace package manifest")?; + let manifest = toml::from_str::(&manifest) + .context("failed to parse workspace package manifest")?; + + *specifier = DependencySpecifiers::Pesde(PesdeDependencySpecifier { + name: spec.name.clone(), + version: match spec.version_type { + VersionType::Wildcard => VersionReq::STAR, + v => VersionReq::parse(&format!("{v}{}", manifest.version)) + .context(format!("failed to parse version for {v}"))?, + }, + index: Some( + manifest + .indices + .get(DEFAULT_INDEX_NAME) + .context("missing default index in workspace package manifest")? + .to_string(), + ), + target: Some(manifest.target.kind()), + }); + } } } diff --git a/src/cli/commands/run.rs b/src/cli/commands/run.rs index 4a2302c..e8688fd 100644 --- a/src/cli/commands/run.rs +++ b/src/cli/commands/run.rs @@ -4,18 +4,18 @@ use anyhow::Context; use clap::Args; use relative_path::RelativePathBuf; +use crate::cli::IsUpToDate; use pesde::{ names::{PackageName, PackageNames}, + source::traits::PackageRef, Project, PACKAGES_CONTAINER_NAME, }; -use crate::cli::IsUpToDate; - #[derive(Debug, Args)] pub struct RunCommand { /// The package name, script name, or path to a script to run #[arg(index = 1)] - package_or_script: String, + package_or_script: Option, /// Arguments to pass to the script #[arg(index = 2, last = true)] @@ -30,14 +30,25 @@ impl RunCommand { .arg(path) .arg("--") .args(&self.args) - .current_dir(project.path()) + .current_dir(project.package_dir()) .status() .expect("failed to run script"); std::process::exit(status.code().unwrap_or(1)) }; - if let Ok(pkg_name) = self.package_or_script.parse::() { + let package_or_script = match self.package_or_script { + Some(package_or_script) => package_or_script, + None => { + if let Some(script_path) = project.deser_manifest()?.target.bin_path() { + run(script_path.to_path(project.package_dir())); + } + + anyhow::bail!("no package or script specified") + } + }; + + if let Ok(pkg_name) = package_or_script.parse::() { let graph = if project.is_up_to_date(true)? { project.deser_lockfile()?.graph } else { @@ -55,12 +66,14 @@ impl RunCommand { anyhow::bail!("package has no bin path"); }; - let base_folder = node - .node - .base_folder(project.deser_manifest()?.target.kind(), true); + let base_folder = project + .deser_manifest()? + .target + .kind() + .packages_folder(&node.node.pkg_ref.target_kind()); let container_folder = node.node.container_folder( &project - .path() + .package_dir() .join(base_folder) .join(PACKAGES_CONTAINER_NAME), &pkg_name, @@ -72,13 +85,13 @@ impl RunCommand { } if let Ok(manifest) = project.deser_manifest() { - if let Some(script_path) = manifest.scripts.get(&self.package_or_script) { - run(script_path.to_path(project.path())) + if let Some(script_path) = manifest.scripts.get(&package_or_script) { + run(script_path.to_path(project.package_dir())) } }; - let relative_path = RelativePathBuf::from(self.package_or_script); - let path = relative_path.to_path(project.path()); + let relative_path = RelativePathBuf::from(package_or_script); + let path = relative_path.to_path(project.package_dir()); if !path.exists() { anyhow::bail!("path does not exist: {}", path.display()); diff --git a/src/cli/files.rs b/src/cli/files.rs index 4e6c8b6..b882b57 100644 --- a/src/cli/files.rs +++ b/src/cli/files.rs @@ -1,7 +1,6 @@ use std::path::Path; pub fn make_executable>(_path: P) -> anyhow::Result<()> { - // TODO: test if this actually works #[cfg(unix)] { use anyhow::Context; diff --git a/src/download.rs b/src/download.rs index 50e01f1..17bad39 100644 --- a/src/download.rs +++ b/src/download.rs @@ -44,8 +44,13 @@ impl Project { let container_folder = node.container_folder( &self - .path() - .join(node.base_folder(manifest.target.kind(), true)) + .package_dir() + .join( + manifest + .target + .kind() + .packages_folder(&node.pkg_ref.target_kind()), + ) .join(PACKAGES_CONTAINER_NAME), name, version_id.version(), diff --git a/src/lib.rs b/src/lib.rs index 74d3bfd..52e5c1d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,7 @@ #[cfg(not(any(feature = "roblox", feature = "lune", feature = "luau")))] compile_error!("at least one of the features `roblox`, `lune`, or `luau` must be enabled"); -use crate::lockfile::Lockfile; +use crate::{lockfile::Lockfile, manifest::Manifest}; use gix::sec::identity::Account; use std::{ collections::HashMap, @@ -108,7 +108,8 @@ impl AuthConfig { /// The main struct of the pesde library, representing a project #[derive(Debug, Clone)] pub struct Project { - path: PathBuf, + package_dir: PathBuf, + workspace_dir: Option, data_dir: PathBuf, auth_config: AuthConfig, cas_dir: PathBuf, @@ -116,23 +117,30 @@ pub struct Project { impl Project { /// Create a new `Project` - pub fn new, Q: AsRef, R: AsRef>( - path: P, - data_dir: Q, - cas_dir: R, + pub fn new, Q: AsRef, R: AsRef, S: AsRef>( + package_dir: P, + workspace_dir: Option, + data_dir: R, + cas_dir: S, auth_config: AuthConfig, ) -> Self { Project { - path: path.as_ref().to_path_buf(), + package_dir: package_dir.as_ref().to_path_buf(), + workspace_dir: workspace_dir.map(|d| d.as_ref().to_path_buf()), data_dir: data_dir.as_ref().to_path_buf(), auth_config, cas_dir: cas_dir.as_ref().to_path_buf(), } } - /// Access the path - pub fn path(&self) -> &Path { - &self.path + /// Access the package directory + pub fn package_dir(&self) -> &Path { + &self.package_dir + } + + /// Access the workspace directory + pub fn workspace_dir(&self) -> Option<&Path> { + self.workspace_dir.as_deref() } /// Access the data directory @@ -152,37 +160,71 @@ impl Project { /// Read the manifest file pub fn read_manifest(&self) -> Result { - let string = std::fs::read_to_string(self.path.join(MANIFEST_FILE_NAME))?; + let string = std::fs::read_to_string(self.package_dir.join(MANIFEST_FILE_NAME))?; Ok(string) } /// Deserialize the manifest file - pub fn deser_manifest(&self) -> Result { - let string = std::fs::read_to_string(self.path.join(MANIFEST_FILE_NAME))?; + pub fn deser_manifest(&self) -> Result { + let string = std::fs::read_to_string(self.package_dir.join(MANIFEST_FILE_NAME))?; Ok(toml::from_str(&string)?) } /// Write the manifest file pub fn write_manifest>(&self, manifest: S) -> Result<(), std::io::Error> { - std::fs::write(self.path.join(MANIFEST_FILE_NAME), manifest.as_ref()) + std::fs::write(self.package_dir.join(MANIFEST_FILE_NAME), manifest.as_ref()) } /// Deserialize the lockfile pub fn deser_lockfile(&self) -> Result { - let string = std::fs::read_to_string(self.path.join(LOCKFILE_FILE_NAME))?; + let string = std::fs::read_to_string(self.package_dir.join(LOCKFILE_FILE_NAME))?; Ok(toml::from_str(&string)?) } /// Write the lockfile pub fn write_lockfile(&self, lockfile: Lockfile) -> Result<(), errors::LockfileWriteError> { let string = toml::to_string(&lockfile)?; - std::fs::write(self.path.join(LOCKFILE_FILE_NAME), string)?; + std::fs::write(self.package_dir.join(LOCKFILE_FILE_NAME), string)?; Ok(()) } + + /// Get the workspace members + pub fn workspace_members>( + &self, + dir: P, + ) -> Result, errors::WorkspaceMembersError> { + let dir = dir.as_ref().to_path_buf(); + let manifest = std::fs::read_to_string(dir.join(MANIFEST_FILE_NAME)) + .map_err(|e| errors::WorkspaceMembersError::ManifestMissing(dir.to_path_buf(), e))?; + let manifest = toml::from_str::(&manifest) + .map_err(|e| errors::WorkspaceMembersError::ManifestDeser(dir.to_path_buf(), e))?; + + let members = manifest + .workspace_members + .into_iter() + .map(|glob| dir.join(glob)) + .map(|path| glob::glob(&path.as_os_str().to_string_lossy())) + .collect::, _>>()? + .into_iter() + .flat_map(|paths| paths.into_iter()) + .collect::, _>>()?; + + members + .into_iter() + .map(|path| { + let manifest = std::fs::read_to_string(path.join(MANIFEST_FILE_NAME)) + .map_err(|e| errors::WorkspaceMembersError::ManifestMissing(path.clone(), e))?; + let manifest = toml::from_str::(&manifest) + .map_err(|e| errors::WorkspaceMembersError::ManifestDeser(path.clone(), e))?; + Ok((path, manifest)) + }) + .collect::>() + } } /// Errors that can occur when using the pesde library pub mod errors { + use std::path::PathBuf; use thiserror::Error; /// Errors that can occur when reading the manifest file @@ -223,4 +265,29 @@ pub mod errors { #[error("error serializing lockfile")] Serde(#[from] toml::ser::Error), } + + /// Errors that can occur when finding workspace members + #[derive(Debug, Error)] + #[non_exhaustive] + pub enum WorkspaceMembersError { + /// The manifest file could not be found + #[error("missing manifest file at {0}")] + ManifestMissing(PathBuf, #[source] std::io::Error), + + /// An error occurred deserializing the manifest file + #[error("error deserializing manifest file at {0}")] + ManifestDeser(PathBuf, #[source] toml::de::Error), + + /// An error occurred interacting with the filesystem + #[error("error interacting with the filesystem")] + Io(#[from] std::io::Error), + + /// An invalid glob pattern was found + #[error("invalid glob pattern")] + Glob(#[from] glob::PatternError), + + /// An error occurred while globbing + #[error("error globbing")] + Globbing(#[from] glob::GlobError), + } } diff --git a/src/linking/generator.rs b/src/linking/generator.rs index 0cb5b02..6b7a1f9 100644 --- a/src/linking/generator.rs +++ b/src/linking/generator.rs @@ -92,10 +92,10 @@ pub fn get_lib_require_path( ) -> String { let path = pathdiff::diff_paths(destination_dir, base_dir).unwrap(); let path = if use_new_structure { - log::debug!("using new structure for require path"); + log::debug!("using new structure for require path with {:?}", lib_file); lib_file.to_path(path) } else { - log::debug!("using old structure for require path"); + log::debug!("using old structure for require path with {:?}", lib_file); path }; diff --git a/src/linking/mod.rs b/src/linking/mod.rs index 12ab1f8..46d3785 100644 --- a/src/linking/mod.rs +++ b/src/linking/mod.rs @@ -44,8 +44,13 @@ impl Project { let container_folder = node.node.container_folder( &self - .path() - .join(node.node.base_folder(manifest.target.kind(), true)) + .package_dir() + .join( + manifest + .target + .kind() + .packages_folder(&node.node.pkg_ref.target_kind()), + ) .join(PACKAGES_CONTAINER_NAME), name, version_id.version(), @@ -99,7 +104,7 @@ impl Project { execute_script( ScriptName::RobloxSyncConfigGenerator, - &script_path.to_path(self.path()), + &script_path.to_path(self.package_dir()), std::iter::once(container_folder.as_os_str()) .chain(build_files.iter().map(OsStr::new)), self, @@ -117,56 +122,64 @@ impl Project { for (name, versions) in graph { for (version_id, node) in versions { - let base_folder = create_and_canonicalize( - self.path().join( - self.path() - .join(node.node.base_folder(manifest.target.kind(), true)), - ), - )?; - let packages_container_folder = base_folder.join(PACKAGES_CONTAINER_NAME); + let node_container_folder = { + let base_folder = create_and_canonicalize( + self.package_dir().join( + manifest + .target + .kind() + .packages_folder(&node.node.pkg_ref.target_kind()), + ), + )?; + let packages_container_folder = base_folder.join(PACKAGES_CONTAINER_NAME); - let container_folder = node.node.container_folder( - &packages_container_folder, - name, - version_id.version(), - ); + let container_folder = node.node.container_folder( + &packages_container_folder, + name, + version_id.version(), + ); - if let Some((alias, types)) = package_types - .get(name) - .and_then(|v| v.get(version_id)) - .and_then(|types| node.node.direct.as_ref().map(|(alias, _)| (alias, types))) - { - 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(), + if let Some((alias, types)) = package_types + .get(name) + .and_then(|v| v.get(version_id)) + .and_then(|types| { + node.node.direct.as_ref().map(|(alias, _)| (alias, types)) + }) + { + 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(), + ), + types, ), - types, - ), - )?; - }; + )?; + }; - 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( - &generator::get_bin_require_path( - &base_folder, - bin_file, - &container_folder, + 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( + &generator::get_bin_require_path( + &base_folder, + bin_file, + &container_folder, + ), ), - ), - )?; + )?; + } } - } + + container_folder + }; for (dependency_name, (dependency_version_id, dependency_alias)) in &node.node.dependencies @@ -185,9 +198,19 @@ impl Project { continue; }; + let packages_container_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 linker_folder = create_and_canonicalize( - container_folder - .join(dependency_node.node.base_folder(node.target.kind(), false)), + node_container_folder + .join(node.node.base_folder(dependency_node.target.kind())), )?; write_cas( @@ -203,7 +226,7 @@ impl Project { dependency_name, dependency_version_id.version(), ), - node.node.pkg_ref.use_new_structure(), + dependency_node.node.pkg_ref.use_new_structure(), ), package_types .get(dependency_name) diff --git a/src/lockfile.rs b/src/lockfile.rs index 8fd1cb9..493bdcc 100644 --- a/src/lockfile.rs +++ b/src/lockfile.rs @@ -10,6 +10,7 @@ use crate::{ version_id::VersionId, }, }; +use relative_path::RelativePathBuf; use semver::Version; use serde::{Deserialize, Serialize}; use std::{ @@ -36,10 +37,9 @@ pub struct DependencyGraphNode { } impl DependencyGraphNode { - /// Returns the folder to store dependencies in for this package - pub fn base_folder(&self, project_target: TargetKind, is_top_level: bool) -> String { - if is_top_level || self.pkg_ref.use_new_structure() { - project_target.packages_folder(&self.pkg_ref.target_kind()) + pub(crate) fn base_folder(&self, project_target: TargetKind) -> String { + if self.pkg_ref.use_new_structure() { + self.pkg_ref.target_kind().packages_folder(&project_target) } else { "..".to_string() } @@ -62,8 +62,7 @@ impl DependencyGraphNode { /// A graph of `DependencyGraphNode`s pub type DependencyGraph = Graph; -/// Inserts a node into a graph -pub fn insert_node( +pub(crate) fn insert_node( graph: &mut DependencyGraph, name: PackageNames, version: VersionId, @@ -128,6 +127,10 @@ pub struct Lockfile { #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub overrides: BTreeMap, + /// The workspace members + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub workspace: BTreeMap, + /// The graph of dependencies #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub graph: DownloadedGraph, diff --git a/src/main.rs b/src/main.rs index 84dd1bb..9e5f3c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,14 @@ -use std::{fs::create_dir_all, path::PathBuf}; - use anyhow::Context; use clap::Parser; use colored::Colorize; use indicatif::MultiProgress; use indicatif_log_bridge::LogWrapper; - use pesde::{AuthConfig, Project, MANIFEST_FILE_NAME}; +use std::{ + collections::HashSet, + fs::create_dir_all, + path::{Path, PathBuf}, +}; use crate::cli::{ auth::get_token, @@ -67,23 +69,6 @@ fn get_root(path: &std::path::Path) -> PathBuf { fn run() -> anyhow::Result<()> { let cwd = std::env::current_dir().expect("failed to get current working directory"); - let project_root_dir = 'finder: { - let mut project_root = cwd.clone(); - - while project_root.components().count() > 1 { - if project_root.join(MANIFEST_FILE_NAME).exists() { - break 'finder project_root; - } - - if let Some(parent) = project_root.parent() { - project_root = parent.to_path_buf(); - } else { - break; - } - } - - cwd.clone() - }; #[cfg(windows)] 'scripts: { @@ -105,17 +90,85 @@ fn run() -> anyhow::Result<()> { break 'scripts; } + // the bin script will search for the project root itself, so we do that to ensure + // consistency across platforms, since the script is executed using a shebang + // on unix systems let status = std::process::Command::new("lune") .arg("run") .arg(exe.with_extension("")) + .arg("--") .args(std::env::args_os().skip(1)) - .current_dir(project_root_dir) + .current_dir(cwd) .status() .expect("failed to run lune"); std::process::exit(status.code().unwrap()); } + let (project_root_dir, project_workspace_dir) = 'finder: { + let mut current_path = Some(cwd.clone()); + let mut project_root = None::; + let mut workspace_dir = None::; + + fn get_workspace_members(path: &Path) -> anyhow::Result> { + let manifest = std::fs::read_to_string(path.join(MANIFEST_FILE_NAME)) + .context("failed to read manifest")?; + let manifest: pesde::manifest::Manifest = + toml::from_str(&manifest).context("failed to parse manifest")?; + + if manifest.workspace_members.is_empty() { + return Ok(HashSet::new()); + } + + manifest + .workspace_members + .iter() + .map(|member| path.join(member)) + .map(|p| glob::glob(&p.to_string_lossy())) + .collect::, _>>() + .context("invalid glob patterns")? + .into_iter() + .flat_map(|paths| paths.into_iter()) + .collect::, _>>() + .context("failed to expand glob patterns") + } + + while let Some(path) = current_path { + current_path = path.parent().map(|p| p.to_path_buf()); + + if !path.join(MANIFEST_FILE_NAME).exists() { + continue; + } + + match (project_root.as_ref(), workspace_dir.as_ref()) { + (Some(project_root), Some(workspace_dir)) => { + break 'finder (project_root.clone(), Some(workspace_dir.clone())); + } + + (Some(project_root), None) => { + if get_workspace_members(&path)?.contains(project_root) { + workspace_dir = Some(path); + } + } + + (None, None) => { + if get_workspace_members(&path)?.contains(&cwd) { + // initializing a new member of a workspace + break 'finder (cwd, Some(path)); + } else { + project_root = Some(path); + } + } + + (None, Some(_)) => unreachable!(), + } + } + + // we mustn't expect the project root to be found, as that would + // disable the ability to run pesde in a non-project directory (for example to init it) + (project_root.unwrap_or_else(|| cwd.clone()), workspace_dir) + }; + let multi = { let logger = pretty_env_logger::formatted_builder() .parse_env(pretty_env_logger::env_logger::Env::default().default_filter_or("info")) @@ -143,6 +196,7 @@ fn run() -> anyhow::Result<()> { let project = Project::new( project_root_dir, + project_workspace_dir, data_dir, cas_dir, AuthConfig::new() diff --git a/src/manifest/mod.rs b/src/manifest/mod.rs index ffc09b7..57e0c58 100644 --- a/src/manifest/mod.rs +++ b/src/manifest/mod.rs @@ -74,6 +74,9 @@ pub struct Manifest { #[serde(default, skip_serializing)] /// Which version of the pesde CLI this package uses pub pesde_version: Option, + /// A list of globs pointing to workspace members' directories + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub workspace_members: Vec, /// The standard dependencies of the package #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] diff --git a/src/patches.rs b/src/patches.rs index 32814c9..b570c91 100644 --- a/src/patches.rs +++ b/src/patches.rs @@ -1,4 +1,7 @@ -use crate::{lockfile::DownloadedGraph, Project, MANIFEST_FILE_NAME, PACKAGES_CONTAINER_NAME}; +use crate::{ + lockfile::DownloadedGraph, source::traits::PackageRef, Project, MANIFEST_FILE_NAME, + PACKAGES_CONTAINER_NAME, +}; use git2::{ApplyLocation, ApplyOptions, Diff, DiffFormat, DiffLineType, Repository, Signature}; use relative_path::RelativePathBuf; use std::{fs::read, path::Path}; @@ -73,7 +76,7 @@ impl Project { for (name, versions) in manifest.patches { for (version_id, patch_path) in versions { - let patch_path = patch_path.to_path(self.path()); + let patch_path = patch_path.to_path(self.package_dir()); let patch = Diff::from_buffer(&read(&patch_path).map_err(|e| { errors::ApplyPatchesError::PatchReadError(patch_path.clone(), e) })?)?; @@ -87,8 +90,13 @@ impl Project { let container_folder = node.node.container_folder( &self - .path() - .join(node.node.base_folder(manifest.target.kind(), true)) + .package_dir() + .join( + manifest + .target + .kind() + .packages_folder(&node.node.pkg_ref.target_kind()), + ) .join(PACKAGES_CONTAINER_NAME), &name, version_id.version(), diff --git a/src/resolver.rs b/src/resolver.rs index 2a55b1b..a09aaf6 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -40,6 +40,11 @@ impl Project { continue; }; + if matches!(specifier, DependencySpecifiers::Workspace(_)) { + // workspace dependencies must always be resolved brand new + continue; + } + if all_specifiers .remove(&(specifier.clone(), node.ty)) .is_none() @@ -180,6 +185,9 @@ impl Project { DependencySpecifiers::Git(specifier) => PackageSources::Git( crate::source::git::GitPackageSource::new(specifier.repo.clone()), ), + DependencySpecifiers::Workspace(_) => { + PackageSources::Workspace(crate::source::workspace::WorkspacePackageSource) + } }; if refreshed_sources.insert(source.clone()) { diff --git a/src/scripts.rs b/src/scripts.rs index 4c311e0..99f92af 100644 --- a/src/scripts.rs +++ b/src/scripts.rs @@ -45,7 +45,7 @@ pub(crate) fn execute_script, S: AsRef>( .arg(script_path.as_os_str()) .arg("--") .args(args) - .current_dir(project.path()) + .current_dir(project.package_dir()) .stdin(Stdio::inherit()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) diff --git a/src/source/fs.rs b/src/source/fs.rs index f4f5d86..6b04a0d 100644 --- a/src/source/fs.rs +++ b/src/source/fs.rs @@ -4,12 +4,15 @@ use std::{ path::{Path, PathBuf}, }; +use crate::{ + manifest::target::TargetKind, + source::{IGNORED_DIRS, IGNORED_FILES}, + util::hash, +}; use relative_path::RelativePathBuf; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -use crate::util::hash; - /// A file system entry #[derive(Debug, Clone, Serialize, Deserialize)] pub enum FSEntry { @@ -23,8 +26,14 @@ pub enum FSEntry { /// A package's file system #[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(transparent)] -pub struct PackageFS(pub(crate) BTreeMap); +// don't need to differentiate between CAS and non-CAS, since non-CAS won't be serialized +#[serde(untagged)] +pub enum PackageFS { + /// A package stored in the CAS + CAS(BTreeMap), + /// A package that's to be copied + Copy(PathBuf, TargetKind), +} pub(crate) fn store_in_cas>( cas_dir: P, @@ -92,6 +101,40 @@ pub(crate) fn store_reader_in_cas>( Ok(hash) } +fn copy_dir_all( + src: impl AsRef, + dst: impl AsRef, + target: TargetKind, +) -> std::io::Result<()> { + std::fs::create_dir_all(&dst)?; + 'outer: for entry in std::fs::read_dir(src)? { + let entry = entry?; + let ty = entry.file_type()?; + let file_name = entry.file_name().to_string_lossy().to_string(); + + if ty.is_dir() { + if IGNORED_DIRS.contains(&file_name.as_ref()) { + continue; + } + + for other_target in TargetKind::VARIANTS { + if target.packages_folder(other_target) == file_name { + continue 'outer; + } + } + + copy_dir_all(entry.path(), dst.as_ref().join(&file_name), target)?; + } else { + if IGNORED_FILES.contains(&file_name.as_ref()) { + continue; + } + + std::fs::copy(entry.path(), dst.as_ref().join(file_name))?; + } + } + Ok(()) +} + impl PackageFS { /// Write the package to the given destination pub fn write_to, Q: AsRef>( @@ -100,27 +143,34 @@ impl PackageFS { cas_path: Q, link: bool, ) -> std::io::Result<()> { - for (path, entry) in &self.0 { - let path = path.to_path(destination.as_ref()); + match self { + PackageFS::CAS(entries) => { + for (path, entry) in entries { + let path = path.to_path(destination.as_ref()); - match entry { - FSEntry::File(hash) => { - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent)?; - } + match entry { + FSEntry::File(hash) => { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } - let (prefix, rest) = hash.split_at(2); - let cas_file_path = cas_path.as_ref().join(prefix).join(rest); + let (prefix, rest) = hash.split_at(2); + let cas_file_path = cas_path.as_ref().join(prefix).join(rest); - if link { - std::fs::hard_link(cas_file_path, path)?; - } else { - std::fs::copy(cas_file_path, path)?; + if link { + std::fs::hard_link(cas_file_path, path)?; + } else { + std::fs::copy(cas_file_path, path)?; + } + } + FSEntry::Directory => { + std::fs::create_dir_all(path)?; + } } } - FSEntry::Directory => { - std::fs::create_dir_all(path)?; - } + } + PackageFS::Copy(src, target) => { + copy_dir_all(src, destination, *target)?; } } @@ -133,6 +183,10 @@ impl PackageFS { file_hash: H, cas_path: P, ) -> Option { + if !matches!(self, PackageFS::CAS(_)) { + return None; + } + let (prefix, rest) = file_hash.as_ref().split_at(2); let cas_file_path = cas_path.as_ref().join(prefix).join(rest); std::fs::read_to_string(cas_file_path).ok() diff --git a/src/source/git/mod.rs b/src/source/git/mod.rs index 9b1795b..a318ac1 100644 --- a/src/source/git/mod.rs +++ b/src/source/git/mod.rs @@ -162,6 +162,7 @@ impl PackageSource for GitPackageSource { ); } DependencySpecifiers::Git(_) => {} + DependencySpecifiers::Workspace(_) => todo!(), } Ok((alias, (spec, ty))) @@ -262,21 +263,26 @@ impl PackageSource for GitPackageSource { errors::DownloadError::DeserializeFile(Box::new(self.repo_url.clone()), e) })?; - let manifest = match fs.0.get(&RelativePathBuf::from(MANIFEST_FILE_NAME)) { - Some(FSEntry::File(hash)) => match fs - .read_file(hash, project.cas_dir()) - .map(|m| toml::de::from_str::(&m)) - { - Some(Ok(m)) => Some(m), - Some(Err(e)) => { - return Err(errors::DownloadError::DeserializeFile( - Box::new(self.repo_url.clone()), - e, - )) + let manifest = match &fs { + PackageFS::CAS(entries) => { + match entries.get(&RelativePathBuf::from(MANIFEST_FILE_NAME)) { + Some(FSEntry::File(hash)) => match fs + .read_file(hash, project.cas_dir()) + .map(|m| toml::de::from_str::(&m)) + { + Some(Ok(m)) => Some(m), + Some(Err(e)) => { + return Err(errors::DownloadError::DeserializeFile( + Box::new(self.repo_url.clone()), + e, + )) + } + None => None, + }, + _ => None, } - None => None, - }, - _ => None, + } + _ => unreachable!("the package fs should be CAS"), }; let target = match manifest { @@ -380,7 +386,7 @@ impl PackageSource for GitPackageSource { None => None, }; - let fs = PackageFS(entries); + let fs = PackageFS::CAS(entries); let target = match manifest { Some(manifest) => manifest.target, diff --git a/src/source/mod.rs b/src/source/mod.rs index 9a3105e..7254db8 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -29,6 +29,8 @@ pub mod version_id; /// The Wally package source #[cfg(feature = "wally-compat")] pub mod wally; +/// The workspace package source +pub mod workspace; /// Files that will not be stored when downloading a package. These are only files which break pesde's functionality, or are meaningless and possibly heavy (e.g. `.DS_Store`) pub const IGNORED_FILES: &[&str] = &["foreman.toml", "aftman.toml", "rokit.toml", ".DS_Store"]; @@ -49,6 +51,8 @@ pub enum PackageSources { Wally(wally::WallyPackageSource), /// A Git package source Git(git::GitPackageSource), + /// A workspace package source + Workspace(workspace::WorkspacePackageSource), } impl PackageSource for PackageSources { @@ -64,6 +68,7 @@ impl PackageSource for PackageSources { #[cfg(feature = "wally-compat")] PackageSources::Wally(source) => source.refresh(project).map_err(Into::into), PackageSources::Git(source) => source.refresh(project).map_err(Into::into), + PackageSources::Workspace(source) => source.refresh(project).map_err(Into::into), } } @@ -71,11 +76,11 @@ impl PackageSource for PackageSources { &self, specifier: &Self::Specifier, project: &Project, - project_target: TargetKind, + package_target: TargetKind, ) -> Result, Self::ResolveError> { match (self, specifier) { (PackageSources::Pesde(source), DependencySpecifiers::Pesde(specifier)) => source - .resolve(specifier, project, project_target) + .resolve(specifier, project, package_target) .map(|(name, results)| { ( name, @@ -89,7 +94,7 @@ impl PackageSource for PackageSources { #[cfg(feature = "wally-compat")] (PackageSources::Wally(source), DependencySpecifiers::Wally(specifier)) => source - .resolve(specifier, project, project_target) + .resolve(specifier, project, package_target) .map(|(name, results)| { ( name, @@ -102,7 +107,7 @@ impl PackageSource for PackageSources { .map_err(Into::into), (PackageSources::Git(source), DependencySpecifiers::Git(specifier)) => source - .resolve(specifier, project, project_target) + .resolve(specifier, project, package_target) .map(|(name, results)| { ( name, @@ -114,6 +119,23 @@ impl PackageSource for PackageSources { }) .map_err(Into::into), + (PackageSources::Workspace(source), DependencySpecifiers::Workspace(specifier)) => { + source + .resolve(specifier, project, package_target) + .map(|(name, results)| { + ( + name, + results + .into_iter() + .map(|(version, pkg_ref)| { + (version, PackageRefs::Workspace(pkg_ref)) + }) + .collect(), + ) + }) + .map_err(Into::into) + } + _ => Err(errors::ResolveError::Mismatch), } } @@ -138,6 +160,10 @@ impl PackageSource for PackageSources { .download(pkg_ref, project, reqwest) .map_err(Into::into), + (PackageSources::Workspace(source), PackageRefs::Workspace(pkg_ref)) => source + .download(pkg_ref, project, reqwest) + .map_err(Into::into), + _ => Err(errors::DownloadError::Mismatch), } } @@ -154,6 +180,10 @@ pub mod errors { /// A git-based package source failed to refresh #[error("error refreshing pesde package source")] GitBased(#[from] crate::source::git_index::errors::RefreshError), + + /// A workspace package source failed to refresh + #[error("error refreshing workspace package source")] + Workspace(#[from] crate::source::workspace::errors::RefreshError), } /// Errors that can occur when resolving a package @@ -176,6 +206,10 @@ pub mod errors { /// A Git package source failed to resolve #[error("error resolving git package")] Git(#[from] crate::source::git::errors::ResolveError), + + /// A workspace package source failed to resolve + #[error("error resolving workspace package")] + Workspace(#[from] crate::source::workspace::errors::ResolveError), } /// Errors that can occur when downloading a package @@ -198,5 +232,9 @@ pub mod errors { /// A Git package source failed to download #[error("error downloading git package")] Git(#[from] crate::source::git::errors::DownloadError), + + /// A workspace package source failed to download + #[error("error downloading workspace package")] + Workspace(#[from] crate::source::workspace::errors::DownloadError), } } diff --git a/src/source/pesde/mod.rs b/src/source/pesde/mod.rs index 3f1c482..858e789 100644 --- a/src/source/pesde/mod.rs +++ b/src/source/pesde/mod.rs @@ -194,7 +194,7 @@ impl PackageSource for PesdePackageSource { &self, specifier: &Self::Specifier, project: &Project, - project_target: TargetKind, + package_target: TargetKind, ) -> Result, Self::ResolveError> { let (scope, name) = specifier.name.as_str(); let string = match self.read_file([scope, name], project, None) { @@ -221,7 +221,7 @@ impl PackageSource for PesdePackageSource { specifier.version.matches(version) && specifier .target - .map_or(project_target.is_compatible_with(target), |t| t == *target) + .map_or(package_target.is_compatible_with(target), |t| t == *target) }) .map(|(id, entry)| { let version = id.version().clone(); @@ -331,7 +331,7 @@ impl PackageSource for PesdePackageSource { entries.insert(path, FSEntry::File(hash)); } - let fs = PackageFS(entries); + let fs = PackageFS::CAS(entries); if let Some(parent) = index_file.parent() { std::fs::create_dir_all(parent)?; diff --git a/src/source/refs.rs b/src/source/refs.rs index 7d24443..9cfc7f7 100644 --- a/src/source/refs.rs +++ b/src/source/refs.rs @@ -16,6 +16,8 @@ pub enum PackageRefs { Wally(crate::source::wally::pkg_ref::WallyPackageRef), /// A Git package reference Git(crate::source::git::pkg_ref::GitPackageRef), + /// A workspace package reference + Workspace(crate::source::workspace::pkg_ref::WorkspacePackageRef), } impl PackageRefs { @@ -37,6 +39,7 @@ impl PackageRef for PackageRefs { #[cfg(feature = "wally-compat")] PackageRefs::Wally(pkg_ref) => pkg_ref.dependencies(), PackageRefs::Git(pkg_ref) => pkg_ref.dependencies(), + PackageRefs::Workspace(pkg_ref) => pkg_ref.dependencies(), } } @@ -46,6 +49,7 @@ impl PackageRef for PackageRefs { #[cfg(feature = "wally-compat")] PackageRefs::Wally(pkg_ref) => pkg_ref.use_new_structure(), PackageRefs::Git(pkg_ref) => pkg_ref.use_new_structure(), + PackageRefs::Workspace(pkg_ref) => pkg_ref.use_new_structure(), } } @@ -55,6 +59,7 @@ impl PackageRef for PackageRefs { #[cfg(feature = "wally-compat")] PackageRefs::Wally(pkg_ref) => pkg_ref.target_kind(), PackageRefs::Git(pkg_ref) => pkg_ref.target_kind(), + PackageRefs::Workspace(pkg_ref) => pkg_ref.target_kind(), } } @@ -64,6 +69,7 @@ impl PackageRef for PackageRefs { #[cfg(feature = "wally-compat")] PackageRefs::Wally(pkg_ref) => pkg_ref.source(), PackageRefs::Git(pkg_ref) => pkg_ref.source(), + PackageRefs::Workspace(pkg_ref) => pkg_ref.source(), } } } diff --git a/src/source/specifiers.rs b/src/source/specifiers.rs index cf0d10e..9f7a603 100644 --- a/src/source/specifiers.rs +++ b/src/source/specifiers.rs @@ -13,6 +13,8 @@ pub enum DependencySpecifiers { Wally(crate::source::wally::specifier::WallyDependencySpecifier), /// A Git dependency specifier Git(crate::source::git::specifier::GitDependencySpecifier), + /// A workspace dependency specifier + Workspace(crate::source::workspace::specifier::WorkspaceDependencySpecifier), } impl DependencySpecifier for DependencySpecifiers {} @@ -23,6 +25,7 @@ impl Display for DependencySpecifiers { #[cfg(feature = "wally-compat")] DependencySpecifiers::Wally(specifier) => write!(f, "{specifier}"), DependencySpecifiers::Git(specifier) => write!(f, "{specifier}"), + DependencySpecifiers::Workspace(specifier) => write!(f, "{specifier}"), } } } diff --git a/src/source/wally/compat_util.rs b/src/source/wally/compat_util.rs index 926f1a1..4226eb5 100644 --- a/src/source/wally/compat_util.rs +++ b/src/source/wally/compat_util.rs @@ -33,7 +33,7 @@ pub(crate) fn find_lib_path( let result = execute_script( ScriptName::SourcemapGenerator, - &script_path.to_path(&project.path), + &script_path.to_path(&project.package_dir), [package_dir], project, true, diff --git a/src/source/wally/mod.rs b/src/source/wally/mod.rs index 9e586da..f137d1e 100644 --- a/src/source/wally/mod.rs +++ b/src/source/wally/mod.rs @@ -94,7 +94,7 @@ impl PackageSource for WallyPackageSource { &self, specifier: &Self::Specifier, project: &Project, - _project_target: TargetKind, + _package_target: TargetKind, ) -> Result, Self::ResolveError> { let (scope, name) = specifier.name.as_str(); let string = match self.read_file([scope, name], project, None) { @@ -238,7 +238,7 @@ impl PackageSource for WallyPackageSource { entries.insert(path, FSEntry::File(hash)); } - let fs = PackageFS(entries); + let fs = PackageFS::CAS(entries); if let Some(parent) = index_file.parent() { std::fs::create_dir_all(parent).map_err(errors::DownloadError::WriteIndex)?; diff --git a/src/source/workspace/mod.rs b/src/source/workspace/mod.rs new file mode 100644 index 0000000..901c4d1 --- /dev/null +++ b/src/source/workspace/mod.rs @@ -0,0 +1,173 @@ +use crate::{ + manifest::target::{Target, TargetKind}, + names::PackageNames, + source::{ + fs::PackageFS, specifiers::DependencySpecifiers, traits::PackageSource, + version_id::VersionId, workspace::pkg_ref::WorkspacePackageRef, ResolveResult, + }, + Project, DEFAULT_INDEX_NAME, +}; +use relative_path::RelativePathBuf; +use reqwest::blocking::Client; +use std::collections::BTreeMap; + +/// The workspace package reference +pub mod pkg_ref; +/// The workspace dependency specifier +pub mod specifier; + +/// The workspace package source +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct WorkspacePackageSource; + +impl PackageSource for WorkspacePackageSource { + type Specifier = specifier::WorkspaceDependencySpecifier; + type Ref = WorkspacePackageRef; + type RefreshError = errors::RefreshError; + type ResolveError = errors::ResolveError; + type DownloadError = errors::DownloadError; + + fn refresh(&self, _project: &Project) -> Result<(), Self::RefreshError> { + // no-op + Ok(()) + } + + fn resolve( + &self, + specifier: &Self::Specifier, + project: &Project, + _package_target: TargetKind, + ) -> Result, Self::ResolveError> { + let (path, manifest) = 'finder: { + let workspace_dir = project + .workspace_dir + .as_ref() + .unwrap_or(&project.package_dir); + + for (path, manifest) in project.workspace_members(workspace_dir)? { + if manifest.name == specifier.name { + break 'finder (path, manifest); + } + } + + return Err(errors::ResolveError::NoWorkspaceMember( + specifier.name.to_string(), + )); + }; + + Ok(( + PackageNames::Pesde(manifest.name.clone()), + BTreeMap::from([( + VersionId::new(manifest.version.clone(), manifest.target.kind()), + WorkspacePackageRef { + // workspace_dir is guaranteed to be Some by the workspace_members method + // strip_prefix is guaranteed to be Some by same method + // from_path is guaranteed to be Ok because we just stripped the absolute path + path: RelativePathBuf::from_path( + path.strip_prefix(project.workspace_dir.clone().unwrap()) + .unwrap(), + ) + .unwrap(), + dependencies: manifest + .all_dependencies()? + .into_iter() + .map(|(alias, (mut spec, ty))| { + match &mut spec { + DependencySpecifiers::Pesde(spec) => { + let index_name = + spec.index.as_deref().unwrap_or(DEFAULT_INDEX_NAME); + + spec.index = Some( + manifest + .indices + .get(index_name) + .ok_or(errors::ResolveError::IndexNotFound( + index_name.to_string(), + manifest.name.to_string(), + ))? + .to_string(), + ) + } + #[cfg(feature = "wally-compat")] + DependencySpecifiers::Wally(spec) => { + let index_name = + spec.index.as_deref().unwrap_or(DEFAULT_INDEX_NAME); + + spec.index = Some( + manifest + .wally_indices + .get(index_name) + .ok_or(errors::ResolveError::IndexNotFound( + index_name.to_string(), + manifest.name.to_string(), + ))? + .to_string(), + ) + } + DependencySpecifiers::Git(_) => {} + DependencySpecifiers::Workspace(_) => {} + } + + Ok((alias, (spec, ty))) + }) + .collect::>()?, + target: manifest.target, + }, + )]), + )) + } + + fn download( + &self, + pkg_ref: &Self::Ref, + project: &Project, + _reqwest: &Client, + ) -> Result<(PackageFS, Target), Self::DownloadError> { + let path = pkg_ref.path.to_path(project.workspace_dir.clone().unwrap()); + + Ok(( + PackageFS::Copy(path, pkg_ref.target.kind()), + pkg_ref.target.clone(), + )) + } +} + +/// Errors that can occur when using a workspace package source +pub mod errors { + use thiserror::Error; + + /// Errors that can occur when refreshing the workspace package source + #[derive(Debug, Error)] + #[non_exhaustive] + pub enum RefreshError {} + + /// Errors that can occur when resolving a workspace package + #[derive(Debug, Error)] + #[non_exhaustive] + pub enum ResolveError { + /// An error occurred reading the workspace members + #[error("failed to read workspace members")] + ReadWorkspaceMembers(#[from] crate::errors::WorkspaceMembersError), + + /// No workspace member was found with the given name + #[error("no workspace member found with name {0}")] + NoWorkspaceMember(String), + + /// An error occurred getting all dependencies + #[error("failed to get all dependencies")] + AllDependencies(#[from] crate::manifest::errors::AllDependenciesError), + + /// An index of a member package was not found + #[error("index {0} not found in member {1}")] + IndexNotFound(String, String), + } + + /// Errors that can occur when downloading a workspace package + #[derive(Debug, Error)] + #[non_exhaustive] + pub enum DownloadError { + /// An error occurred reading the workspace members + #[error("failed to read workspace members")] + ReadWorkspaceMembers(#[from] std::io::Error), + } +} diff --git a/src/source/workspace/pkg_ref.rs b/src/source/workspace/pkg_ref.rs new file mode 100644 index 0000000..06c3dd7 --- /dev/null +++ b/src/source/workspace/pkg_ref.rs @@ -0,0 +1,40 @@ +use relative_path::RelativePathBuf; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +use crate::{ + manifest::{ + target::{Target, TargetKind}, + DependencyType, + }, + source::{workspace::WorkspacePackageSource, DependencySpecifiers, PackageRef, PackageSources}, +}; + +/// A workspace package reference +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +pub struct WorkspacePackageRef { + /// The path of the package + pub path: RelativePathBuf, + /// The dependencies of the package + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub dependencies: BTreeMap, + /// The target of the package + pub target: Target, +} +impl PackageRef for WorkspacePackageRef { + fn dependencies(&self) -> &BTreeMap { + &self.dependencies + } + + fn use_new_structure(&self) -> bool { + true + } + + fn target_kind(&self) -> TargetKind { + self.target.kind() + } + + fn source(&self) -> PackageSources { + PackageSources::Workspace(WorkspacePackageSource) + } +} diff --git a/src/source/workspace/specifier.rs b/src/source/workspace/specifier.rs new file mode 100644 index 0000000..15f31a3 --- /dev/null +++ b/src/source/workspace/specifier.rs @@ -0,0 +1,78 @@ +use crate::{names::PackageName, source::DependencySpecifier}; +use serde::{Deserialize, Serialize}; +use serde_with::{DeserializeFromStr, SerializeDisplay}; +use std::{fmt::Display, str::FromStr}; + +/// The specifier for a workspace dependency +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] +pub struct WorkspaceDependencySpecifier { + /// The name of the workspace package + #[serde(rename = "workspace")] + pub name: PackageName, + /// The version type to use when publishing the package + #[serde(default, rename = "version")] + pub version_type: VersionType, +} +impl DependencySpecifier for WorkspaceDependencySpecifier {} + +impl Display for WorkspaceDependencySpecifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "workspace:{}{}", self.version_type, self.name) + } +} + +/// The type of version to use when publishing a package +#[derive( + Debug, SerializeDisplay, DeserializeFromStr, Clone, Copy, PartialEq, Eq, Hash, Default, +)] +pub enum VersionType { + /// The "^" version type + #[default] + Caret, + /// The "~" version type + Tilde, + /// The "=" version type + Exact, + /// The "*" version type + Wildcard, +} + +impl Display for VersionType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + VersionType::Caret => write!(f, "^"), + VersionType::Tilde => write!(f, "~"), + VersionType::Exact => write!(f, "="), + VersionType::Wildcard => write!(f, "*"), + } + } +} + +impl FromStr for VersionType { + type Err = errors::VersionTypeFromStr; + + fn from_str(s: &str) -> Result { + match s { + "^" => Ok(VersionType::Caret), + "~" => Ok(VersionType::Tilde), + "=" => Ok(VersionType::Exact), + "*" => Ok(VersionType::Wildcard), + _ => Err(errors::VersionTypeFromStr::InvalidVersionType( + s.to_string(), + )), + } + } +} + +/// Errors that can occur when using a version type +pub mod errors { + use thiserror::Error; + + /// Errors that can occur when parsing a version type + #[derive(Debug, Error)] + pub enum VersionTypeFromStr { + /// The version type is invalid + #[error("invalid version type: {0}")] + InvalidVersionType(String), + } +}