feat: implement workspace/monorepo support

This commit is contained in:
daimond113 2024-09-03 16:01:48 +02:00
parent bd7e1452b0
commit f1ce6283d8
No known key found for this signature in database
GPG key ID: 3A8ECE51328B513C
34 changed files with 880 additions and 202 deletions

11
Cargo.lock generated
View file

@ -2335,6 +2335,12 @@ dependencies = [
"thiserror", "thiserror",
] ]
[[package]]
name = "glob"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]] [[package]]
name = "governor" name = "governor"
version = "0.6.3" version = "0.6.3"
@ -2855,9 +2861,9 @@ dependencies = [
[[package]] [[package]]
name = "keyring" name = "keyring"
version = "3.2.0" version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73b9af47ded4df3067484d7d45758ca2b36bd083bf6d024c2952bbd8af1cdaa4" checksum = "030a9b84bb2a2f3673d4c8b8236091ed5d8f6b66a56d8085471d8abd5f3c6a80"
dependencies = [ dependencies = [
"byteorder", "byteorder",
"dbus-secret-service", "dbus-secret-service",
@ -3506,6 +3512,7 @@ dependencies = [
"full_moon", "full_moon",
"git2", "git2",
"gix", "gix",
"glob",
"indicatif", "indicatif",
"indicatif-log-bridge", "indicatif-log-bridge",
"inquire", "inquire",

View file

@ -43,14 +43,14 @@ required-features = ["bin"]
uninlined_format_args = "warn" uninlined_format_args = "warn"
[dependencies] [dependencies]
serde = { version = "1.0.204", features = ["derive"] } serde = { version = "1.0.209", features = ["derive"] }
toml = "0.8.19" toml = "0.8.19"
serde_with = "3.9.0" serde_with = "3.9.0"
gix = { version = "0.66.0", default-features = false, features = ["blocking-http-transport-reqwest-rust-tls", "revparse-regex", "credentials"] } 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"] } 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" tar = "0.4.41"
flate2 = "1.0.31" flate2 = "1.0.33"
pathdiff = "0.2.1" pathdiff = "0.2.1"
relative-path = { version = "1.9.3", features = ["serde"] } relative-path = { version = "1.9.3", features = ["serde"] }
log = "0.4.22" log = "0.4.22"
@ -63,23 +63,24 @@ url = { version = "2.5.2", features = ["serde"] }
chrono = { version = "0.4.38", features = ["serde"] } chrono = { version = "0.4.38", features = ["serde"] }
sha2 = "0.10.8" sha2 = "0.10.8"
tempfile = "3.12.0" tempfile = "3.12.0"
glob = "0.3.1"
# TODO: remove this when gitoxide adds support for: committing, pushing, adding # TODO: remove this when gitoxide adds support for: committing, pushing, adding
git2 = { version = "0.19.0", optional = true } git2 = { version = "0.19.0", optional = true }
zip = { version = "2.1.6", optional = true } zip = { version = "2.2.0", optional = true }
serde_json = { version = "1.0.122", optional = true } serde_json = { version = "1.0.127", optional = true }
anyhow = { version = "1.0.86", optional = true } anyhow = { version = "1.0.86", optional = true }
open = { version = "5.3.0", 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 } colored = { version = "2.1.0", optional = true }
toml_edit = { version = "0.22.20", 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 } dirs = { version = "5.0.1", optional = true }
pretty_env_logger = { version = "0.5.0", optional = true } pretty_env_logger = { version = "0.5.0", optional = true }
indicatif = { version = "0.17.8", 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 } inquire = { version = "0.7.5", optional = true }
[target.'cfg(target_os = "windows")'.dependencies] [target.'cfg(target_os = "windows")'.dependencies]

View file

@ -298,13 +298,6 @@ pub async fn publish_package(
{ {
return Err(Error::InvalidArchive); 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) => { DependencySpecifiers::Wally(specifier) => {
if !config.wally_allowed { if !config.wally_allowed {
@ -325,6 +318,10 @@ pub async fn publish_package(
return Err(Error::InvalidArchive); return Err(Error::InvalidArchive);
} }
} }
DependencySpecifiers::Workspace(_) => {
// workspace specifiers are to be transformed into Pesde specifiers by the sender
return Err(Error::InvalidArchive);
}
} }
} }

View file

@ -1,5 +1,3 @@
use std::{env::current_dir, fs::create_dir_all, sync::Mutex};
use actix_cors::Cors; use actix_cors::Cors;
use actix_governor::{Governor, GovernorConfigBuilder}; use actix_governor::{Governor, GovernorConfigBuilder};
use actix_web::{ use actix_web::{
@ -9,6 +7,7 @@ use actix_web::{
}; };
use log::info; use log::info;
use rusty_s3::{Bucket, Credentials, UrlStyle}; use rusty_s3::{Bucket, Credentials, UrlStyle};
use std::{env::current_dir, fs::create_dir_all, path::PathBuf, sync::Mutex};
use pesde::{ use pesde::{
source::{pesde::PesdePackageSource, traits::PackageSource}, source::{pesde::PesdePackageSource, traits::PackageSource},
@ -78,6 +77,7 @@ async fn run(with_sentry: bool) -> std::io::Result<()> {
let project = Project::new( let project = Project::new(
&cwd, &cwd,
None::<PathBuf>,
data_dir.join("project"), data_dir.join("project"),
&cwd, &cwd,
AuthConfig::new().with_git_credentials(Some(gix::sec::identity::Account { AuthConfig::new().with_git_credentials(Some(gix::sec::identity::Account {

View file

@ -197,6 +197,7 @@ impl AddCommand {
println!("added git {}#{} to {}", spec.repo, spec.rev, dependency_key); println!("added git {}#{} to {}", spec.repo, spec.rev, dependency_key);
} }
DependencySpecifiers::Workspace(_) => todo!(),
} }
project project

View file

@ -77,7 +77,7 @@ impl ExecuteCommand {
.arg(bin_path.to_path(tempdir.path())) .arg(bin_path.to_path(tempdir.path()))
.arg("--") .arg("--")
.args(&self.args) .args(&self.args)
.current_dir(project.path()) .current_dir(project.package_dir())
.status() .status()
.context("failed to run script")?; .context("failed to run script")?;

View file

@ -133,7 +133,9 @@ impl InitCommand {
.prompt() .prompt()
.unwrap() .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::create_dir_all(&folder).context("failed to create scripts folder")?;
std::fs::write( std::fs::write(

View file

@ -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::{ use std::{
collections::{BTreeSet, HashSet}, collections::{BTreeSet, HashSet},
sync::Arc, sync::Arc,
time::Duration, 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)] #[derive(Debug, Args)]
pub struct InstallCommand { pub struct InstallCommand {
/// The amount of threads to use for downloading /// The amount of threads to use for downloading
@ -44,7 +42,7 @@ fn bin_link_file(alias: &str) -> String {
let prefix = String::new(); let prefix = String::new();
#[cfg(unix)] #[cfg(unix)]
let prefix = "#!/usr/bin/env -S lune run\n"; let prefix = "#!/usr/bin/env -S lune run\n";
// TODO: reimplement workspace support in this
format!( format!(
r#"{prefix}local process = require("@lune/process") r#"{prefix}local process = require("@lune/process")
local fs = require("@lune/fs") local fs = require("@lune/fs")
@ -113,7 +111,7 @@ impl InstallCommand {
if deleted_folders.insert(folder.to_string()) { if deleted_folders.insert(folder.to_string()) {
log::debug!("deleting the {folder} folder"); 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() .err()
.filter(|e| e.kind() != std::io::ErrorKind::NotFound) .filter(|e| e.kind() != std::io::ErrorKind::NotFound)
{ {
@ -150,7 +148,7 @@ impl InstallCommand {
"{msg} {bar:40.208/166} {pos}/{len} {percent}% {elapsed_precise}", "{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)); 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) let downloaded_graph = Arc::into_inner(downloaded_graph)
.unwrap() .unwrap()
@ -229,6 +230,46 @@ impl InstallCommand {
version: manifest.version, version: manifest.version,
target: manifest.target.kind(), target: manifest.target.kind(),
overrides: manifest.overrides, 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::<Result<_, _>>()
.context("failed to install workspace member's dependencies")?,
},
graph: downloaded_graph, graph: downloaded_graph,
}) })

View file

@ -35,7 +35,10 @@ impl OutdatedCommand {
continue; continue;
}; };
if matches!(specifier, DependencySpecifiers::Git(_)) { if matches!(
specifier,
DependencySpecifiers::Git(_) | DependencySpecifiers::Workspace(_)
) {
continue; continue;
} }
@ -55,6 +58,7 @@ impl OutdatedCommand {
spec.version = VersionReq::STAR; spec.version = VersionReq::STAR;
} }
DependencySpecifiers::Git(_) => {} DependencySpecifiers::Git(_) => {}
DependencySpecifiers::Workspace(_) => {}
}; };
} }

View file

@ -55,7 +55,7 @@ impl PatchCommitCommand {
let patch = create_patch(&self.directory).context("failed to create patch")?; let patch = create_patch(&self.directory).context("failed to create patch")?;
std::fs::remove_dir_all(self.directory).context("failed to remove patch directory")?; 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")?; std::fs::create_dir_all(&patches_dir).context("failed to create patches directory")?;
let patch_file_name = format!("{}-{}.patch", name.escaped(), version_id.escaped(),); let patch_file_name = format!("{}-{}.patch", name.escaped(), version_id.escaped(),);

View file

@ -7,13 +7,17 @@ use anyhow::Context;
use clap::Args; use clap::Args;
use colored::Colorize; use colored::Colorize;
use reqwest::StatusCode; use reqwest::StatusCode;
use semver::VersionReq;
use tempfile::tempfile; use tempfile::tempfile;
use pesde::{ use pesde::{
manifest::target::Target, manifest::target::Target,
scripts::ScriptName, scripts::ScriptName,
source::{ source::{
pesde::PesdePackageSource, specifiers::DependencySpecifiers, traits::PackageSource, pesde::{specifier::PesdeDependencySpecifier, PesdePackageSource},
specifiers::DependencySpecifiers,
traits::PackageSource,
workspace::{specifier::VersionType, WorkspacePackageSource},
IGNORED_DIRS, IGNORED_FILES, IGNORED_DIRS, IGNORED_FILES,
}, },
Project, DEFAULT_INDEX_NAME, MANIFEST_FILE_NAME, Project, DEFAULT_INDEX_NAME, MANIFEST_FILE_NAME,
@ -52,9 +56,10 @@ impl PublishCommand {
#[cfg(feature = "roblox")] #[cfg(feature = "roblox")]
let mut display_build_files: Vec<String> = vec![]; let mut display_build_files: Vec<String> = vec![];
let (lib_path, bin_path) = ( let (lib_path, bin_path, target_kind) = (
manifest.target.lib_path().cloned(), manifest.target.lib_path().cloned(),
manifest.target.bin_path().cloned(), manifest.target.bin_path().cloned(),
manifest.target.kind(),
); );
#[cfg(feature = "roblox")] #[cfg(feature = "roblox")]
@ -124,7 +129,7 @@ impl PublishCommand {
for (name, path) in [("lib path", lib_path), ("bin path", bin_path)] { for (name, path) in [("lib path", lib_path), ("bin path", bin_path)] {
let Some(export_path) = path else { continue }; 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() { if !export_path.exists() {
anyhow::bail!("{name} points to non-existent file"); anyhow::bail!("{name} points to non-existent file");
} }
@ -146,7 +151,7 @@ impl PublishCommand {
} }
let first_part = export_path let first_part = export_path
.strip_prefix(project.path()) .strip_prefix(project.package_dir())
.context(format!("{name} not within project directory"))? .context(format!("{name} not within project directory"))?
.components() .components()
.next() .next()
@ -177,7 +182,7 @@ impl PublishCommand {
} }
for included_name in &manifest.includes { 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() { if !included_path.exists() {
anyhow::bail!("included file {included_name} does not exist"); anyhow::bail!("included file {included_name} does not exist");
@ -216,7 +221,7 @@ impl PublishCommand {
continue; 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() { if !build_file_path.exists() {
anyhow::bail!("build file {build_file} does not exist"); anyhow::bail!("build file {build_file} does not exist");
@ -281,6 +286,45 @@ impl PublishCommand {
DependencySpecifiers::Git(_) => { DependencySpecifiers::Git(_) => {
has_git = true; 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::<pesde::manifest::Manifest>(&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()),
});
}
} }
} }

View file

@ -4,18 +4,18 @@ use anyhow::Context;
use clap::Args; use clap::Args;
use relative_path::RelativePathBuf; use relative_path::RelativePathBuf;
use crate::cli::IsUpToDate;
use pesde::{ use pesde::{
names::{PackageName, PackageNames}, names::{PackageName, PackageNames},
source::traits::PackageRef,
Project, PACKAGES_CONTAINER_NAME, Project, PACKAGES_CONTAINER_NAME,
}; };
use crate::cli::IsUpToDate;
#[derive(Debug, Args)] #[derive(Debug, Args)]
pub struct RunCommand { pub struct RunCommand {
/// The package name, script name, or path to a script to run /// The package name, script name, or path to a script to run
#[arg(index = 1)] #[arg(index = 1)]
package_or_script: String, package_or_script: Option<String>,
/// Arguments to pass to the script /// Arguments to pass to the script
#[arg(index = 2, last = true)] #[arg(index = 2, last = true)]
@ -30,14 +30,25 @@ impl RunCommand {
.arg(path) .arg(path)
.arg("--") .arg("--")
.args(&self.args) .args(&self.args)
.current_dir(project.path()) .current_dir(project.package_dir())
.status() .status()
.expect("failed to run script"); .expect("failed to run script");
std::process::exit(status.code().unwrap_or(1)) std::process::exit(status.code().unwrap_or(1))
}; };
if let Ok(pkg_name) = self.package_or_script.parse::<PackageName>() { 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::<PackageName>() {
let graph = if project.is_up_to_date(true)? { let graph = if project.is_up_to_date(true)? {
project.deser_lockfile()?.graph project.deser_lockfile()?.graph
} else { } else {
@ -55,12 +66,14 @@ impl RunCommand {
anyhow::bail!("package has no bin path"); anyhow::bail!("package has no bin path");
}; };
let base_folder = node let base_folder = project
.node .deser_manifest()?
.base_folder(project.deser_manifest()?.target.kind(), true); .target
.kind()
.packages_folder(&node.node.pkg_ref.target_kind());
let container_folder = node.node.container_folder( let container_folder = node.node.container_folder(
&project &project
.path() .package_dir()
.join(base_folder) .join(base_folder)
.join(PACKAGES_CONTAINER_NAME), .join(PACKAGES_CONTAINER_NAME),
&pkg_name, &pkg_name,
@ -72,13 +85,13 @@ impl RunCommand {
} }
if let Ok(manifest) = project.deser_manifest() { if let Ok(manifest) = project.deser_manifest() {
if let Some(script_path) = manifest.scripts.get(&self.package_or_script) { if let Some(script_path) = manifest.scripts.get(&package_or_script) {
run(script_path.to_path(project.path())) run(script_path.to_path(project.package_dir()))
} }
}; };
let relative_path = RelativePathBuf::from(self.package_or_script); let relative_path = RelativePathBuf::from(package_or_script);
let path = relative_path.to_path(project.path()); let path = relative_path.to_path(project.package_dir());
if !path.exists() { if !path.exists() {
anyhow::bail!("path does not exist: {}", path.display()); anyhow::bail!("path does not exist: {}", path.display());

View file

@ -1,7 +1,6 @@
use std::path::Path; use std::path::Path;
pub fn make_executable<P: AsRef<Path>>(_path: P) -> anyhow::Result<()> { pub fn make_executable<P: AsRef<Path>>(_path: P) -> anyhow::Result<()> {
// TODO: test if this actually works
#[cfg(unix)] #[cfg(unix)]
{ {
use anyhow::Context; use anyhow::Context;

View file

@ -44,8 +44,13 @@ impl Project {
let container_folder = node.container_folder( let container_folder = node.container_folder(
&self &self
.path() .package_dir()
.join(node.base_folder(manifest.target.kind(), true)) .join(
manifest
.target
.kind()
.packages_folder(&node.pkg_ref.target_kind()),
)
.join(PACKAGES_CONTAINER_NAME), .join(PACKAGES_CONTAINER_NAME),
name, name,
version_id.version(), version_id.version(),

View file

@ -6,7 +6,7 @@
#[cfg(not(any(feature = "roblox", feature = "lune", feature = "luau")))] #[cfg(not(any(feature = "roblox", feature = "lune", feature = "luau")))]
compile_error!("at least one of the features `roblox`, `lune`, or `luau` must be enabled"); 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 gix::sec::identity::Account;
use std::{ use std::{
collections::HashMap, collections::HashMap,
@ -108,7 +108,8 @@ impl AuthConfig {
/// The main struct of the pesde library, representing a project /// The main struct of the pesde library, representing a project
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Project { pub struct Project {
path: PathBuf, package_dir: PathBuf,
workspace_dir: Option<PathBuf>,
data_dir: PathBuf, data_dir: PathBuf,
auth_config: AuthConfig, auth_config: AuthConfig,
cas_dir: PathBuf, cas_dir: PathBuf,
@ -116,23 +117,30 @@ pub struct Project {
impl Project { impl Project {
/// Create a new `Project` /// Create a new `Project`
pub fn new<P: AsRef<Path>, Q: AsRef<Path>, R: AsRef<Path>>( pub fn new<P: AsRef<Path>, Q: AsRef<Path>, R: AsRef<Path>, S: AsRef<Path>>(
path: P, package_dir: P,
data_dir: Q, workspace_dir: Option<Q>,
cas_dir: R, data_dir: R,
cas_dir: S,
auth_config: AuthConfig, auth_config: AuthConfig,
) -> Self { ) -> Self {
Project { 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(), data_dir: data_dir.as_ref().to_path_buf(),
auth_config, auth_config,
cas_dir: cas_dir.as_ref().to_path_buf(), cas_dir: cas_dir.as_ref().to_path_buf(),
} }
} }
/// Access the path /// Access the package directory
pub fn path(&self) -> &Path { pub fn package_dir(&self) -> &Path {
&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 /// Access the data directory
@ -152,37 +160,71 @@ impl Project {
/// Read the manifest file /// Read the manifest file
pub fn read_manifest(&self) -> Result<String, errors::ManifestReadError> { pub fn read_manifest(&self) -> Result<String, errors::ManifestReadError> {
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) Ok(string)
} }
/// Deserialize the manifest file /// Deserialize the manifest file
pub fn deser_manifest(&self) -> Result<manifest::Manifest, errors::ManifestReadError> { pub fn deser_manifest(&self) -> Result<Manifest, errors::ManifestReadError> {
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(toml::from_str(&string)?) Ok(toml::from_str(&string)?)
} }
/// Write the manifest file /// Write the manifest file
pub fn write_manifest<S: AsRef<[u8]>>(&self, manifest: S) -> Result<(), std::io::Error> { pub fn write_manifest<S: AsRef<[u8]>>(&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 /// Deserialize the lockfile
pub fn deser_lockfile(&self) -> Result<Lockfile, errors::LockfileReadError> { pub fn deser_lockfile(&self) -> Result<Lockfile, errors::LockfileReadError> {
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)?) Ok(toml::from_str(&string)?)
} }
/// Write the lockfile /// Write the lockfile
pub fn write_lockfile(&self, lockfile: Lockfile) -> Result<(), errors::LockfileWriteError> { pub fn write_lockfile(&self, lockfile: Lockfile) -> Result<(), errors::LockfileWriteError> {
let string = toml::to_string(&lockfile)?; 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(()) Ok(())
} }
/// Get the workspace members
pub fn workspace_members<P: AsRef<Path>>(
&self,
dir: P,
) -> Result<HashMap<PathBuf, Manifest>, 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>(&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::<Result<Vec<_>, _>>()?
.into_iter()
.flat_map(|paths| paths.into_iter())
.collect::<Result<Vec<_>, _>>()?;
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>(&manifest)
.map_err(|e| errors::WorkspaceMembersError::ManifestDeser(path.clone(), e))?;
Ok((path, manifest))
})
.collect::<Result<_, _>>()
}
} }
/// Errors that can occur when using the pesde library /// Errors that can occur when using the pesde library
pub mod errors { pub mod errors {
use std::path::PathBuf;
use thiserror::Error; use thiserror::Error;
/// Errors that can occur when reading the manifest file /// Errors that can occur when reading the manifest file
@ -223,4 +265,29 @@ pub mod errors {
#[error("error serializing lockfile")] #[error("error serializing lockfile")]
Serde(#[from] toml::ser::Error), 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),
}
} }

View file

@ -92,10 +92,10 @@ pub fn get_lib_require_path(
) -> String { ) -> String {
let path = pathdiff::diff_paths(destination_dir, base_dir).unwrap(); let path = pathdiff::diff_paths(destination_dir, base_dir).unwrap();
let path = if use_new_structure { 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) lib_file.to_path(path)
} else { } else {
log::debug!("using old structure for require path"); log::debug!("using old structure for require path with {:?}", lib_file);
path path
}; };

View file

@ -44,8 +44,13 @@ impl Project {
let container_folder = node.node.container_folder( let container_folder = node.node.container_folder(
&self &self
.path() .package_dir()
.join(node.node.base_folder(manifest.target.kind(), true)) .join(
manifest
.target
.kind()
.packages_folder(&node.node.pkg_ref.target_kind()),
)
.join(PACKAGES_CONTAINER_NAME), .join(PACKAGES_CONTAINER_NAME),
name, name,
version_id.version(), version_id.version(),
@ -99,7 +104,7 @@ impl Project {
execute_script( execute_script(
ScriptName::RobloxSyncConfigGenerator, ScriptName::RobloxSyncConfigGenerator,
&script_path.to_path(self.path()), &script_path.to_path(self.package_dir()),
std::iter::once(container_folder.as_os_str()) std::iter::once(container_folder.as_os_str())
.chain(build_files.iter().map(OsStr::new)), .chain(build_files.iter().map(OsStr::new)),
self, self,
@ -117,56 +122,64 @@ impl Project {
for (name, versions) in graph { for (name, versions) in graph {
for (version_id, node) in versions { for (version_id, node) in versions {
let base_folder = create_and_canonicalize( let node_container_folder = {
self.path().join( let base_folder = create_and_canonicalize(
self.path() self.package_dir().join(
.join(node.node.base_folder(manifest.target.kind(), true)), manifest
), .target
)?; .kind()
let packages_container_folder = base_folder.join(PACKAGES_CONTAINER_NAME); .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( let container_folder = node.node.container_folder(
&packages_container_folder, &packages_container_folder,
name, name,
version_id.version(), version_id.version(),
); );
if let Some((alias, types)) = package_types if let Some((alias, types)) = package_types
.get(name) .get(name)
.and_then(|v| v.get(version_id)) .and_then(|v| v.get(version_id))
.and_then(|types| node.node.direct.as_ref().map(|(alias, _)| (alias, types))) .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")), if let Some(lib_file) = node.target.lib_path() {
self.cas_dir(), write_cas(
&generator::generate_lib_linking_module( base_folder.join(format!("{alias}.luau")),
&generator::get_lib_require_path( self.cas_dir(),
&node.target.kind(), &generator::generate_lib_linking_module(
&base_folder, &generator::get_lib_require_path(
lib_file, &node.target.kind(),
&container_folder, &base_folder,
node.node.pkg_ref.use_new_structure(), lib_file,
&container_folder,
node.node.pkg_ref.use_new_structure(),
),
types,
), ),
types, )?;
), };
)?;
};
if let Some(bin_file) = node.target.bin_path() { if let Some(bin_file) = node.target.bin_path() {
write_cas( write_cas(
base_folder.join(format!("{alias}.bin.luau")), base_folder.join(format!("{alias}.bin.luau")),
self.cas_dir(), self.cas_dir(),
&generator::generate_bin_linking_module( &generator::generate_bin_linking_module(
&generator::get_bin_require_path( &generator::get_bin_require_path(
&base_folder, &base_folder,
bin_file, bin_file,
&container_folder, &container_folder,
),
), ),
), )?;
)?; }
} }
}
container_folder
};
for (dependency_name, (dependency_version_id, dependency_alias)) in for (dependency_name, (dependency_version_id, dependency_alias)) in
&node.node.dependencies &node.node.dependencies
@ -185,9 +198,19 @@ impl Project {
continue; 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( let linker_folder = create_and_canonicalize(
container_folder node_container_folder
.join(dependency_node.node.base_folder(node.target.kind(), false)), .join(node.node.base_folder(dependency_node.target.kind())),
)?; )?;
write_cas( write_cas(
@ -203,7 +226,7 @@ impl Project {
dependency_name, dependency_name,
dependency_version_id.version(), dependency_version_id.version(),
), ),
node.node.pkg_ref.use_new_structure(), dependency_node.node.pkg_ref.use_new_structure(),
), ),
package_types package_types
.get(dependency_name) .get(dependency_name)

View file

@ -10,6 +10,7 @@ use crate::{
version_id::VersionId, version_id::VersionId,
}, },
}; };
use relative_path::RelativePathBuf;
use semver::Version; use semver::Version;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
@ -36,10 +37,9 @@ pub struct DependencyGraphNode {
} }
impl DependencyGraphNode { impl DependencyGraphNode {
/// Returns the folder to store dependencies in for this package pub(crate) fn base_folder(&self, project_target: TargetKind) -> String {
pub fn base_folder(&self, project_target: TargetKind, is_top_level: bool) -> String { if self.pkg_ref.use_new_structure() {
if is_top_level || self.pkg_ref.use_new_structure() { self.pkg_ref.target_kind().packages_folder(&project_target)
project_target.packages_folder(&self.pkg_ref.target_kind())
} else { } else {
"..".to_string() "..".to_string()
} }
@ -62,8 +62,7 @@ impl DependencyGraphNode {
/// A graph of `DependencyGraphNode`s /// A graph of `DependencyGraphNode`s
pub type DependencyGraph = Graph<DependencyGraphNode>; pub type DependencyGraph = Graph<DependencyGraphNode>;
/// Inserts a node into a graph pub(crate) fn insert_node(
pub fn insert_node(
graph: &mut DependencyGraph, graph: &mut DependencyGraph,
name: PackageNames, name: PackageNames,
version: VersionId, version: VersionId,
@ -128,6 +127,10 @@ pub struct Lockfile {
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")] #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub overrides: BTreeMap<OverrideKey, DependencySpecifiers>, pub overrides: BTreeMap<OverrideKey, DependencySpecifiers>,
/// The workspace members
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub workspace: BTreeMap<PackageName, RelativePathBuf>,
/// The graph of dependencies /// The graph of dependencies
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")] #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub graph: DownloadedGraph, pub graph: DownloadedGraph,

View file

@ -1,12 +1,14 @@
use std::{fs::create_dir_all, path::PathBuf};
use anyhow::Context; use anyhow::Context;
use clap::Parser; use clap::Parser;
use colored::Colorize; use colored::Colorize;
use indicatif::MultiProgress; use indicatif::MultiProgress;
use indicatif_log_bridge::LogWrapper; use indicatif_log_bridge::LogWrapper;
use pesde::{AuthConfig, Project, MANIFEST_FILE_NAME}; use pesde::{AuthConfig, Project, MANIFEST_FILE_NAME};
use std::{
collections::HashSet,
fs::create_dir_all,
path::{Path, PathBuf},
};
use crate::cli::{ use crate::cli::{
auth::get_token, auth::get_token,
@ -67,23 +69,6 @@ fn get_root(path: &std::path::Path) -> PathBuf {
fn run() -> anyhow::Result<()> { fn run() -> anyhow::Result<()> {
let cwd = std::env::current_dir().expect("failed to get current working directory"); 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)] #[cfg(windows)]
'scripts: { 'scripts: {
@ -105,17 +90,85 @@ fn run() -> anyhow::Result<()> {
break 'scripts; 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") let status = std::process::Command::new("lune")
.arg("run") .arg("run")
.arg(exe.with_extension("")) .arg(exe.with_extension(""))
.arg("--")
.args(std::env::args_os().skip(1)) .args(std::env::args_os().skip(1))
.current_dir(project_root_dir) .current_dir(cwd)
.status() .status()
.expect("failed to run lune"); .expect("failed to run lune");
std::process::exit(status.code().unwrap()); 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::<PathBuf>;
let mut workspace_dir = None::<PathBuf>;
fn get_workspace_members(path: &Path) -> anyhow::Result<HashSet<PathBuf>> {
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::<Result<Vec<_>, _>>()
.context("invalid glob patterns")?
.into_iter()
.flat_map(|paths| paths.into_iter())
.collect::<Result<HashSet<_>, _>>()
.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 multi = {
let logger = pretty_env_logger::formatted_builder() let logger = pretty_env_logger::formatted_builder()
.parse_env(pretty_env_logger::env_logger::Env::default().default_filter_or("info")) .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( let project = Project::new(
project_root_dir, project_root_dir,
project_workspace_dir,
data_dir, data_dir,
cas_dir, cas_dir,
AuthConfig::new() AuthConfig::new()

View file

@ -74,6 +74,9 @@ pub struct Manifest {
#[serde(default, skip_serializing)] #[serde(default, skip_serializing)]
/// Which version of the pesde CLI this package uses /// Which version of the pesde CLI this package uses
pub pesde_version: Option<Version>, pub pesde_version: Option<Version>,
/// A list of globs pointing to workspace members' directories
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub workspace_members: Vec<String>,
/// The standard dependencies of the package /// The standard dependencies of the package
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")] #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]

View file

@ -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 git2::{ApplyLocation, ApplyOptions, Diff, DiffFormat, DiffLineType, Repository, Signature};
use relative_path::RelativePathBuf; use relative_path::RelativePathBuf;
use std::{fs::read, path::Path}; use std::{fs::read, path::Path};
@ -73,7 +76,7 @@ impl Project {
for (name, versions) in manifest.patches { for (name, versions) in manifest.patches {
for (version_id, patch_path) in versions { 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| { let patch = Diff::from_buffer(&read(&patch_path).map_err(|e| {
errors::ApplyPatchesError::PatchReadError(patch_path.clone(), e) errors::ApplyPatchesError::PatchReadError(patch_path.clone(), e)
})?)?; })?)?;
@ -87,8 +90,13 @@ impl Project {
let container_folder = node.node.container_folder( let container_folder = node.node.container_folder(
&self &self
.path() .package_dir()
.join(node.node.base_folder(manifest.target.kind(), true)) .join(
manifest
.target
.kind()
.packages_folder(&node.node.pkg_ref.target_kind()),
)
.join(PACKAGES_CONTAINER_NAME), .join(PACKAGES_CONTAINER_NAME),
&name, &name,
version_id.version(), version_id.version(),

View file

@ -40,6 +40,11 @@ impl Project {
continue; continue;
}; };
if matches!(specifier, DependencySpecifiers::Workspace(_)) {
// workspace dependencies must always be resolved brand new
continue;
}
if all_specifiers if all_specifiers
.remove(&(specifier.clone(), node.ty)) .remove(&(specifier.clone(), node.ty))
.is_none() .is_none()
@ -180,6 +185,9 @@ impl Project {
DependencySpecifiers::Git(specifier) => PackageSources::Git( DependencySpecifiers::Git(specifier) => PackageSources::Git(
crate::source::git::GitPackageSource::new(specifier.repo.clone()), crate::source::git::GitPackageSource::new(specifier.repo.clone()),
), ),
DependencySpecifiers::Workspace(_) => {
PackageSources::Workspace(crate::source::workspace::WorkspacePackageSource)
}
}; };
if refreshed_sources.insert(source.clone()) { if refreshed_sources.insert(source.clone()) {

View file

@ -45,7 +45,7 @@ pub(crate) fn execute_script<A: IntoIterator<Item = S>, S: AsRef<OsStr>>(
.arg(script_path.as_os_str()) .arg(script_path.as_os_str())
.arg("--") .arg("--")
.args(args) .args(args)
.current_dir(project.path()) .current_dir(project.package_dir())
.stdin(Stdio::inherit()) .stdin(Stdio::inherit())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()) .stderr(Stdio::piped())

View file

@ -4,12 +4,15 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use crate::{
manifest::target::TargetKind,
source::{IGNORED_DIRS, IGNORED_FILES},
util::hash,
};
use relative_path::RelativePathBuf; use relative_path::RelativePathBuf;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use crate::util::hash;
/// A file system entry /// A file system entry
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FSEntry { pub enum FSEntry {
@ -23,8 +26,14 @@ pub enum FSEntry {
/// A package's file system /// A package's file system
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(transparent)] // don't need to differentiate between CAS and non-CAS, since non-CAS won't be serialized
pub struct PackageFS(pub(crate) BTreeMap<RelativePathBuf, FSEntry>); #[serde(untagged)]
pub enum PackageFS {
/// A package stored in the CAS
CAS(BTreeMap<RelativePathBuf, FSEntry>),
/// A package that's to be copied
Copy(PathBuf, TargetKind),
}
pub(crate) fn store_in_cas<P: AsRef<Path>>( pub(crate) fn store_in_cas<P: AsRef<Path>>(
cas_dir: P, cas_dir: P,
@ -92,6 +101,40 @@ pub(crate) fn store_reader_in_cas<P: AsRef<Path>>(
Ok(hash) Ok(hash)
} }
fn copy_dir_all(
src: impl AsRef<Path>,
dst: impl AsRef<Path>,
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 { impl PackageFS {
/// Write the package to the given destination /// Write the package to the given destination
pub fn write_to<P: AsRef<Path>, Q: AsRef<Path>>( pub fn write_to<P: AsRef<Path>, Q: AsRef<Path>>(
@ -100,27 +143,34 @@ impl PackageFS {
cas_path: Q, cas_path: Q,
link: bool, link: bool,
) -> std::io::Result<()> { ) -> std::io::Result<()> {
for (path, entry) in &self.0 { match self {
let path = path.to_path(destination.as_ref()); PackageFS::CAS(entries) => {
for (path, entry) in entries {
let path = path.to_path(destination.as_ref());
match entry { match entry {
FSEntry::File(hash) => { FSEntry::File(hash) => {
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?; std::fs::create_dir_all(parent)?;
} }
let (prefix, rest) = hash.split_at(2); let (prefix, rest) = hash.split_at(2);
let cas_file_path = cas_path.as_ref().join(prefix).join(rest); let cas_file_path = cas_path.as_ref().join(prefix).join(rest);
if link { if link {
std::fs::hard_link(cas_file_path, path)?; std::fs::hard_link(cas_file_path, path)?;
} else { } else {
std::fs::copy(cas_file_path, path)?; 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, file_hash: H,
cas_path: P, cas_path: P,
) -> Option<String> { ) -> Option<String> {
if !matches!(self, PackageFS::CAS(_)) {
return None;
}
let (prefix, rest) = file_hash.as_ref().split_at(2); let (prefix, rest) = file_hash.as_ref().split_at(2);
let cas_file_path = cas_path.as_ref().join(prefix).join(rest); let cas_file_path = cas_path.as_ref().join(prefix).join(rest);
std::fs::read_to_string(cas_file_path).ok() std::fs::read_to_string(cas_file_path).ok()

View file

@ -162,6 +162,7 @@ impl PackageSource for GitPackageSource {
); );
} }
DependencySpecifiers::Git(_) => {} DependencySpecifiers::Git(_) => {}
DependencySpecifiers::Workspace(_) => todo!(),
} }
Ok((alias, (spec, ty))) Ok((alias, (spec, ty)))
@ -262,21 +263,26 @@ impl PackageSource for GitPackageSource {
errors::DownloadError::DeserializeFile(Box::new(self.repo_url.clone()), e) errors::DownloadError::DeserializeFile(Box::new(self.repo_url.clone()), e)
})?; })?;
let manifest = match fs.0.get(&RelativePathBuf::from(MANIFEST_FILE_NAME)) { let manifest = match &fs {
Some(FSEntry::File(hash)) => match fs PackageFS::CAS(entries) => {
.read_file(hash, project.cas_dir()) match entries.get(&RelativePathBuf::from(MANIFEST_FILE_NAME)) {
.map(|m| toml::de::from_str::<Manifest>(&m)) Some(FSEntry::File(hash)) => match fs
{ .read_file(hash, project.cas_dir())
Some(Ok(m)) => Some(m), .map(|m| toml::de::from_str::<Manifest>(&m))
Some(Err(e)) => { {
return Err(errors::DownloadError::DeserializeFile( Some(Ok(m)) => Some(m),
Box::new(self.repo_url.clone()), Some(Err(e)) => {
e, return Err(errors::DownloadError::DeserializeFile(
)) Box::new(self.repo_url.clone()),
e,
))
}
None => None,
},
_ => None,
} }
None => None, }
}, _ => unreachable!("the package fs should be CAS"),
_ => None,
}; };
let target = match manifest { let target = match manifest {
@ -380,7 +386,7 @@ impl PackageSource for GitPackageSource {
None => None, None => None,
}; };
let fs = PackageFS(entries); let fs = PackageFS::CAS(entries);
let target = match manifest { let target = match manifest {
Some(manifest) => manifest.target, Some(manifest) => manifest.target,

View file

@ -29,6 +29,8 @@ pub mod version_id;
/// The Wally package source /// The Wally package source
#[cfg(feature = "wally-compat")] #[cfg(feature = "wally-compat")]
pub mod wally; 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`) /// 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"]; pub const IGNORED_FILES: &[&str] = &["foreman.toml", "aftman.toml", "rokit.toml", ".DS_Store"];
@ -49,6 +51,8 @@ pub enum PackageSources {
Wally(wally::WallyPackageSource), Wally(wally::WallyPackageSource),
/// A Git package source /// A Git package source
Git(git::GitPackageSource), Git(git::GitPackageSource),
/// A workspace package source
Workspace(workspace::WorkspacePackageSource),
} }
impl PackageSource for PackageSources { impl PackageSource for PackageSources {
@ -64,6 +68,7 @@ impl PackageSource for PackageSources {
#[cfg(feature = "wally-compat")] #[cfg(feature = "wally-compat")]
PackageSources::Wally(source) => source.refresh(project).map_err(Into::into), PackageSources::Wally(source) => source.refresh(project).map_err(Into::into),
PackageSources::Git(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, &self,
specifier: &Self::Specifier, specifier: &Self::Specifier,
project: &Project, project: &Project,
project_target: TargetKind, package_target: TargetKind,
) -> Result<ResolveResult<Self::Ref>, Self::ResolveError> { ) -> Result<ResolveResult<Self::Ref>, Self::ResolveError> {
match (self, specifier) { match (self, specifier) {
(PackageSources::Pesde(source), DependencySpecifiers::Pesde(specifier)) => source (PackageSources::Pesde(source), DependencySpecifiers::Pesde(specifier)) => source
.resolve(specifier, project, project_target) .resolve(specifier, project, package_target)
.map(|(name, results)| { .map(|(name, results)| {
( (
name, name,
@ -89,7 +94,7 @@ impl PackageSource for PackageSources {
#[cfg(feature = "wally-compat")] #[cfg(feature = "wally-compat")]
(PackageSources::Wally(source), DependencySpecifiers::Wally(specifier)) => source (PackageSources::Wally(source), DependencySpecifiers::Wally(specifier)) => source
.resolve(specifier, project, project_target) .resolve(specifier, project, package_target)
.map(|(name, results)| { .map(|(name, results)| {
( (
name, name,
@ -102,7 +107,7 @@ impl PackageSource for PackageSources {
.map_err(Into::into), .map_err(Into::into),
(PackageSources::Git(source), DependencySpecifiers::Git(specifier)) => source (PackageSources::Git(source), DependencySpecifiers::Git(specifier)) => source
.resolve(specifier, project, project_target) .resolve(specifier, project, package_target)
.map(|(name, results)| { .map(|(name, results)| {
( (
name, name,
@ -114,6 +119,23 @@ impl PackageSource for PackageSources {
}) })
.map_err(Into::into), .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), _ => Err(errors::ResolveError::Mismatch),
} }
} }
@ -138,6 +160,10 @@ impl PackageSource for PackageSources {
.download(pkg_ref, project, reqwest) .download(pkg_ref, project, reqwest)
.map_err(Into::into), .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), _ => Err(errors::DownloadError::Mismatch),
} }
} }
@ -154,6 +180,10 @@ pub mod errors {
/// A git-based package source failed to refresh /// A git-based package source failed to refresh
#[error("error refreshing pesde package source")] #[error("error refreshing pesde package source")]
GitBased(#[from] crate::source::git_index::errors::RefreshError), 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 /// Errors that can occur when resolving a package
@ -176,6 +206,10 @@ pub mod errors {
/// A Git package source failed to resolve /// A Git package source failed to resolve
#[error("error resolving git package")] #[error("error resolving git package")]
Git(#[from] crate::source::git::errors::ResolveError), 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 /// Errors that can occur when downloading a package
@ -198,5 +232,9 @@ pub mod errors {
/// A Git package source failed to download /// A Git package source failed to download
#[error("error downloading git package")] #[error("error downloading git package")]
Git(#[from] crate::source::git::errors::DownloadError), 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),
} }
} }

View file

@ -194,7 +194,7 @@ impl PackageSource for PesdePackageSource {
&self, &self,
specifier: &Self::Specifier, specifier: &Self::Specifier,
project: &Project, project: &Project,
project_target: TargetKind, package_target: TargetKind,
) -> Result<ResolveResult<Self::Ref>, Self::ResolveError> { ) -> Result<ResolveResult<Self::Ref>, Self::ResolveError> {
let (scope, name) = specifier.name.as_str(); let (scope, name) = specifier.name.as_str();
let string = match self.read_file([scope, name], project, None) { let string = match self.read_file([scope, name], project, None) {
@ -221,7 +221,7 @@ impl PackageSource for PesdePackageSource {
specifier.version.matches(version) specifier.version.matches(version)
&& specifier && specifier
.target .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)| { .map(|(id, entry)| {
let version = id.version().clone(); let version = id.version().clone();
@ -331,7 +331,7 @@ impl PackageSource for PesdePackageSource {
entries.insert(path, FSEntry::File(hash)); entries.insert(path, FSEntry::File(hash));
} }
let fs = PackageFS(entries); let fs = PackageFS::CAS(entries);
if let Some(parent) = index_file.parent() { if let Some(parent) = index_file.parent() {
std::fs::create_dir_all(parent)?; std::fs::create_dir_all(parent)?;

View file

@ -16,6 +16,8 @@ pub enum PackageRefs {
Wally(crate::source::wally::pkg_ref::WallyPackageRef), Wally(crate::source::wally::pkg_ref::WallyPackageRef),
/// A Git package reference /// A Git package reference
Git(crate::source::git::pkg_ref::GitPackageRef), Git(crate::source::git::pkg_ref::GitPackageRef),
/// A workspace package reference
Workspace(crate::source::workspace::pkg_ref::WorkspacePackageRef),
} }
impl PackageRefs { impl PackageRefs {
@ -37,6 +39,7 @@ impl PackageRef for PackageRefs {
#[cfg(feature = "wally-compat")] #[cfg(feature = "wally-compat")]
PackageRefs::Wally(pkg_ref) => pkg_ref.dependencies(), PackageRefs::Wally(pkg_ref) => pkg_ref.dependencies(),
PackageRefs::Git(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")] #[cfg(feature = "wally-compat")]
PackageRefs::Wally(pkg_ref) => pkg_ref.use_new_structure(), PackageRefs::Wally(pkg_ref) => pkg_ref.use_new_structure(),
PackageRefs::Git(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")] #[cfg(feature = "wally-compat")]
PackageRefs::Wally(pkg_ref) => pkg_ref.target_kind(), PackageRefs::Wally(pkg_ref) => pkg_ref.target_kind(),
PackageRefs::Git(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")] #[cfg(feature = "wally-compat")]
PackageRefs::Wally(pkg_ref) => pkg_ref.source(), PackageRefs::Wally(pkg_ref) => pkg_ref.source(),
PackageRefs::Git(pkg_ref) => pkg_ref.source(), PackageRefs::Git(pkg_ref) => pkg_ref.source(),
PackageRefs::Workspace(pkg_ref) => pkg_ref.source(),
} }
} }
} }

View file

@ -13,6 +13,8 @@ pub enum DependencySpecifiers {
Wally(crate::source::wally::specifier::WallyDependencySpecifier), Wally(crate::source::wally::specifier::WallyDependencySpecifier),
/// A Git dependency specifier /// A Git dependency specifier
Git(crate::source::git::specifier::GitDependencySpecifier), Git(crate::source::git::specifier::GitDependencySpecifier),
/// A workspace dependency specifier
Workspace(crate::source::workspace::specifier::WorkspaceDependencySpecifier),
} }
impl DependencySpecifier for DependencySpecifiers {} impl DependencySpecifier for DependencySpecifiers {}
@ -23,6 +25,7 @@ impl Display for DependencySpecifiers {
#[cfg(feature = "wally-compat")] #[cfg(feature = "wally-compat")]
DependencySpecifiers::Wally(specifier) => write!(f, "{specifier}"), DependencySpecifiers::Wally(specifier) => write!(f, "{specifier}"),
DependencySpecifiers::Git(specifier) => write!(f, "{specifier}"), DependencySpecifiers::Git(specifier) => write!(f, "{specifier}"),
DependencySpecifiers::Workspace(specifier) => write!(f, "{specifier}"),
} }
} }
} }

View file

@ -33,7 +33,7 @@ pub(crate) fn find_lib_path(
let result = execute_script( let result = execute_script(
ScriptName::SourcemapGenerator, ScriptName::SourcemapGenerator,
&script_path.to_path(&project.path), &script_path.to_path(&project.package_dir),
[package_dir], [package_dir],
project, project,
true, true,

View file

@ -94,7 +94,7 @@ impl PackageSource for WallyPackageSource {
&self, &self,
specifier: &Self::Specifier, specifier: &Self::Specifier,
project: &Project, project: &Project,
_project_target: TargetKind, _package_target: TargetKind,
) -> Result<crate::source::ResolveResult<Self::Ref>, Self::ResolveError> { ) -> Result<crate::source::ResolveResult<Self::Ref>, Self::ResolveError> {
let (scope, name) = specifier.name.as_str(); let (scope, name) = specifier.name.as_str();
let string = match self.read_file([scope, name], project, None) { let string = match self.read_file([scope, name], project, None) {
@ -238,7 +238,7 @@ impl PackageSource for WallyPackageSource {
entries.insert(path, FSEntry::File(hash)); entries.insert(path, FSEntry::File(hash));
} }
let fs = PackageFS(entries); let fs = PackageFS::CAS(entries);
if let Some(parent) = index_file.parent() { if let Some(parent) = index_file.parent() {
std::fs::create_dir_all(parent).map_err(errors::DownloadError::WriteIndex)?; std::fs::create_dir_all(parent).map_err(errors::DownloadError::WriteIndex)?;

173
src/source/workspace/mod.rs Normal file
View file

@ -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<ResolveResult<Self::Ref>, 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::<Result<_, errors::ResolveError>>()?,
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),
}
}

View file

@ -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<String, (DependencySpecifiers, DependencyType)>,
/// The target of the package
pub target: Target,
}
impl PackageRef for WorkspacePackageRef {
fn dependencies(&self) -> &BTreeMap<String, (DependencySpecifiers, DependencyType)> {
&self.dependencies
}
fn use_new_structure(&self) -> bool {
true
}
fn target_kind(&self) -> TargetKind {
self.target.kind()
}
fn source(&self) -> PackageSources {
PackageSources::Workspace(WorkspacePackageSource)
}
}

View file

@ -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<Self, Self::Err> {
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),
}
}