feat: add path dependencies

Fixes #13
This commit is contained in:
daimond113 2024-12-30 18:33:48 +01:00
parent ccb2924362
commit c3d2c768db
No known key found for this signature in database
GPG key ID: 3A8ECE51328B513C
19 changed files with 341 additions and 24 deletions

View file

@ -137,6 +137,24 @@ pesde add workspace:acme/bar
href="/guides/workspaces/"
/>
## Path Dependencies
Path dependencies are dependencies found anywhere available to the operating system.
They are useful for local development, but are forbidden in published packages.
The path must be absolute and point to a directory containing a `pesde.toml` file.
```toml title="pesde.toml"
[dependencies]
foo = { path = "/home/user/foo" }
```
You can also add a path dependency by running the following command:
```sh
pesde add path:/home/user/foo
```
## Peer Dependencies
Peer dependencies are dependencies that are not installed automatically when

View file

@ -156,8 +156,12 @@ The following formats are supported:
```sh
pesde add pesde/hello
pesde add pesde/hello@1.2.3
pesde add gh#acme/package#main
pesde add https://git.acme.local/package.git#aeff6
pesde add workspace:pesde/hello
pesde add workspace:pesde/hello@1.2.3
pesde add path:/home/user/package
```
## `pesde update`

View file

@ -419,6 +419,19 @@ foo = { workspace = "acme/foo", version = "^" }
href="/guides/workspaces/#workspace-dependencies"
/>
### path
```toml
[dependencies]
foo = { path = "/home/user/foo" }
```
**Path dependencies** contain the following fields:
- `path`: The path to the package on the local filesystem.
Path dependencies are forbidden in published packages.
## `[peer_dependencies]`
The `[peer_dependencies]` section contains a list of peer dependencies for the

View file

@ -348,6 +348,11 @@ pub async fn publish_package(
"non-transformed workspace dependency".into(),
));
}
DependencySpecifiers::Path(_) => {
return Err(Error::InvalidArchive(
"path dependencies are not allowed".into(),
));
}
}
}

View file

@ -80,7 +80,7 @@ pub async fn set_tokens(tokens: Tokens) -> anyhow::Result<()> {
let mut config = read_config().await?;
config.tokens = tokens;
write_config(&config).await.map_err(Into::into)
write_config(&config).await
}
pub async fn set_token(repo: &gix::Url, token: Option<&str>) -> anyhow::Result<()> {

View file

@ -11,10 +11,11 @@ use pesde::{
names::PackageNames,
source::{
git::{specifier::GitDependencySpecifier, GitPackageSource},
path::{specifier::PathDependencySpecifier, PathPackageSource},
pesde::{specifier::PesdeDependencySpecifier, PesdePackageSource},
specifiers::DependencySpecifiers,
traits::{PackageSource, RefreshOptions, ResolveOptions},
workspace::WorkspacePackageSource,
workspace::{specifier::WorkspaceDependencySpecifier, WorkspacePackageSource},
PackageSources,
},
Project, RefreshedSources, DEFAULT_INDEX_NAME,
@ -119,13 +120,15 @@ impl AddCommand {
),
AnyPackageIdentifier::Workspace(VersionedPackageName(name, version)) => (
PackageSources::Workspace(WorkspacePackageSource),
DependencySpecifiers::Workspace(
pesde::source::workspace::specifier::WorkspaceDependencySpecifier {
name: name.clone(),
version: version.clone().unwrap_or_default(),
target: self.target,
},
),
DependencySpecifiers::Workspace(WorkspaceDependencySpecifier {
name: name.clone(),
version: version.clone().unwrap_or_default(),
target: self.target,
}),
),
AnyPackageIdentifier::Path(path) => (
PackageSources::Path(PathPackageSource),
DependencySpecifiers::Path(PathDependencySpecifier { path: path.clone() }),
),
};
@ -187,6 +190,10 @@ impl AddCommand {
.map(|s| s.to_string())
.unwrap_or(url.path.to_string()),
AnyPackageIdentifier::Workspace(versioned) => versioned.0.as_str().1.to_string(),
AnyPackageIdentifier::Path(path) => path
.file_name()
.map(|s| s.to_string_lossy().to_string())
.expect("path has no file name"),
});
let field = &mut manifest[dependency_key]
@ -206,17 +213,17 @@ impl AddCommand {
}
println!(
"added {}@{} {} to {}",
"added {}@{} {} to {dependency_key}",
spec.name,
version_id.version(),
version_id.target(),
dependency_key
version_id.target()
);
}
#[cfg(feature = "wally-compat")]
DependencySpecifiers::Wally(spec) => {
field["wally"] =
toml_edit::value(spec.name.clone().to_string().trim_start_matches("wally#"));
let name_str = spec.name.to_string();
let name_str = name_str.trim_start_matches("wally#");
field["wally"] = toml_edit::value(name_str);
field["version"] = toml_edit::value(format!("^{}", version_id.version()));
if let Some(index) = spec.index.filter(|i| i != DEFAULT_INDEX_NAME) {
@ -224,17 +231,15 @@ impl AddCommand {
}
println!(
"added wally {}@{} to {}",
spec.name,
version_id.version(),
dependency_key
"added wally {name_str}@{} to {dependency_key}",
version_id.version()
);
}
DependencySpecifiers::Git(spec) => {
field["repo"] = toml_edit::value(spec.repo.to_bstring().to_string());
field["rev"] = toml_edit::value(spec.rev.clone());
println!("added git {}#{} to {}", spec.repo, spec.rev, dependency_key);
println!("added git {}#{} to {dependency_key}", spec.repo, spec.rev);
}
DependencySpecifiers::Workspace(spec) => {
field["workspace"] = toml_edit::value(spec.name.clone().to_string());
@ -245,10 +250,15 @@ impl AddCommand {
}
println!(
"added workspace {}@{} to {}",
spec.name, spec.version, dependency_key
"added workspace {}@{} to {dependency_key}",
spec.name, spec.version
);
}
DependencySpecifiers::Path(spec) => {
field["path"] = toml_edit::value(spec.path.to_string_lossy().to_string());
println!("added path {} to {dependency_key}", spec.path.display());
}
}
project

View file

@ -53,7 +53,9 @@ impl OutdatedCommand {
if matches!(
specifier,
DependencySpecifiers::Git(_) | DependencySpecifiers::Workspace(_)
DependencySpecifiers::Git(_)
| DependencySpecifiers::Workspace(_)
| DependencySpecifiers::Path(_)
) {
return Ok(true);
}
@ -79,6 +81,7 @@ impl OutdatedCommand {
}
DependencySpecifiers::Git(_) => {}
DependencySpecifiers::Workspace(_) => {}
DependencySpecifiers::Path(_) => {}
};
}

View file

@ -471,6 +471,9 @@ info: otherwise, the file was deemed unnecessary, if you don't understand why, p
target: Some(spec.target.unwrap_or(manifest.target.kind())),
});
}
DependencySpecifiers::Path(_) => {
anyhow::bail!("path dependencies are not allowed in published packages")
}
}
}

View file

@ -200,6 +200,7 @@ enum AnyPackageIdentifier<V: FromStr = VersionId, N: FromStr = PackageNames> {
PackageName(VersionedPackageName<V, N>),
Url((gix::Url, String)),
Workspace(VersionedPackageName<VersionTypeOrReq, PackageName>),
Path(PathBuf),
}
impl<V: FromStr<Err = E>, E: Into<anyhow::Error>, N: FromStr<Err = F>, F: Into<anyhow::Error>>
@ -218,6 +219,8 @@ impl<V: FromStr<Err = E>, E: Into<anyhow::Error>, N: FromStr<Err = F>, F: Into<a
)))
} else if let Some(rest) = s.strip_prefix("workspace:") {
Ok(AnyPackageIdentifier::Workspace(rest.parse()?))
} else if let Some(rest) = s.strip_prefix("path:") {
Ok(AnyPackageIdentifier::Path(rest.into()))
} else if s.contains(':') {
let (url, rev) = s.split_once('#').context("missing revision")?;

View file

@ -182,8 +182,7 @@ impl Project {
/// Deserialize the manifest file
#[instrument(skip(self), ret(level = "trace"), level = "debug")]
pub async fn deser_manifest(&self) -> Result<Manifest, errors::ManifestReadError> {
let string = fs::read_to_string(self.package_dir().join(MANIFEST_FILE_NAME)).await?;
Ok(toml::from_str(&string)?)
deser_manifest(self.package_dir()).await
}
/// Write the manifest file
@ -345,6 +344,11 @@ impl RefreshedSources {
}
}
async fn deser_manifest(path: &Path) -> Result<Manifest, errors::ManifestReadError> {
let string = fs::read_to_string(path.join(MANIFEST_FILE_NAME)).await?;
Ok(toml::from_str(&string)?)
}
/// Errors that can occur when using the pesde library
pub mod errors {
use std::path::PathBuf;

View file

@ -239,6 +239,9 @@ impl Project {
DependencySpecifiers::Workspace(_) => {
PackageSources::Workspace(crate::source::workspace::WorkspacePackageSource)
}
DependencySpecifiers::Path(_) => {
PackageSources::Path(crate::source::path::PathPackageSource)
}
};
refreshed_sources.refresh(

View file

@ -246,6 +246,11 @@ impl PackageSource for GitPackageSource {
path: Some(path),
})
}
DependencySpecifiers::Path(_) => {
return Err(errors::ResolveError::Path(Box::new(
self.repo_url.clone(),
)))
}
}
Ok((alias, (spec, ty)))
@ -667,6 +672,10 @@ pub mod errors {
/// No path for a workspace member was found in the lockfile
#[error("no path found for workspace member {0} {1} in lockfile for repository {2}")]
NoPathForWorkspaceMember(String, TargetKind, Box<gix::Url>),
/// The package depends on a path package
#[error("the package {0} depends on a path package")]
Path(Box<gix::Url>),
}
/// Errors that can occur when downloading a package from a Git package source

View file

@ -15,6 +15,8 @@ pub mod fs;
pub mod git;
/// Git index-based package source utilities
pub mod git_index;
/// The path package source
pub mod path;
/// The pesde package source
pub mod pesde;
/// Package references
@ -52,6 +54,8 @@ pub enum PackageSources {
Git(git::GitPackageSource),
/// A workspace package source
Workspace(workspace::WorkspacePackageSource),
/// A path package source
Path(path::PathPackageSource),
}
impl PackageSource for PackageSources {
@ -77,6 +81,7 @@ impl PackageSource for PackageSources {
.await
.map_err(Self::RefreshError::Git),
PackageSources::Workspace(source) => source.refresh(options).await.map_err(Into::into),
PackageSources::Path(source) => source.refresh(options).await.map_err(Into::into),
}
}
@ -147,6 +152,20 @@ impl PackageSource for PackageSources {
.map_err(Into::into)
}
(PackageSources::Path(source), DependencySpecifiers::Path(specifier)) => source
.resolve(specifier, options)
.await
.map(|(name, results)| {
(
name,
results
.into_iter()
.map(|(version, pkg_ref)| (version, PackageRefs::Path(pkg_ref)))
.collect(),
)
})
.map_err(Into::into),
_ => Err(errors::ResolveError::Mismatch),
}
}
@ -174,6 +193,10 @@ impl PackageSource for PackageSources {
source.download(pkg_ref, options).await.map_err(Into::into)
}
(PackageSources::Path(source), PackageRefs::Path(pkg_ref)) => {
source.download(pkg_ref, options).await.map_err(Into::into)
}
_ => Err(errors::DownloadError::Mismatch),
}
}
@ -203,6 +226,10 @@ pub mod errors {
/// A workspace package source failed to refresh
#[error("error refreshing workspace package source")]
Workspace(#[from] crate::source::workspace::errors::RefreshError),
/// A path package source failed to refresh
#[error("error refreshing path package source")]
Path(#[from] crate::source::path::errors::RefreshError),
}
/// Errors that can occur when resolving a package
@ -229,6 +256,10 @@ pub mod errors {
/// A workspace package source failed to resolve
#[error("error resolving workspace package")]
Workspace(#[from] crate::source::workspace::errors::ResolveError),
/// A path package source failed to resolve
#[error("error resolving path package")]
Path(#[from] crate::source::path::errors::ResolveError),
}
/// Errors that can occur when downloading a package
@ -255,5 +286,9 @@ pub mod errors {
/// A workspace package source failed to download
#[error("error downloading workspace package")]
Workspace(#[from] crate::source::workspace::errors::DownloadError),
/// A path package source failed to download
#[error("error downloading path package")]
Path(#[from] crate::source::path::errors::DownloadError),
}
}

152
src/source/path/mod.rs Normal file
View file

@ -0,0 +1,152 @@
use crate::{
deser_manifest,
manifest::target::Target,
names::PackageNames,
reporters::DownloadProgressReporter,
source::{
fs::PackageFS,
path::pkg_ref::PathPackageRef,
specifiers::DependencySpecifiers,
traits::{DownloadOptions, PackageSource, ResolveOptions},
version_id::VersionId,
ResolveResult,
},
DEFAULT_INDEX_NAME,
};
use std::collections::BTreeMap;
use tracing::instrument;
/// The path package reference
pub mod pkg_ref;
/// The path dependency specifier
pub mod specifier;
/// The path package source
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PathPackageSource;
impl PackageSource for PathPackageSource {
type Specifier = specifier::PathDependencySpecifier;
type Ref = PathPackageRef;
type RefreshError = errors::RefreshError;
type ResolveError = errors::ResolveError;
type DownloadError = errors::DownloadError;
#[instrument(skip_all, level = "debug")]
async fn resolve(
&self,
specifier: &Self::Specifier,
_options: &ResolveOptions,
) -> Result<ResolveResult<Self::Ref>, Self::ResolveError> {
let manifest = deser_manifest(&specifier.path).await?;
let pkg_ref = PathPackageRef {
path: specifier.path.clone(),
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_else(|| {
errors::ResolveError::IndexNotFound(
index_name.to_string(),
specifier.path.clone(),
)
})?
.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_else(|| {
errors::ResolveError::IndexNotFound(
index_name.to_string(),
specifier.path.clone(),
)
})?
.to_string(),
)
}
DependencySpecifiers::Git(_) => {}
DependencySpecifiers::Workspace(_) => {}
DependencySpecifiers::Path(_) => {}
}
Ok((alias, (spec, ty)))
})
.collect::<Result<_, errors::ResolveError>>()?,
};
Ok((
PackageNames::Pesde(manifest.name),
BTreeMap::from([(
VersionId::new(manifest.version, manifest.target.kind()),
pkg_ref,
)]),
))
}
#[instrument(skip_all, level = "debug")]
async fn download<R: DownloadProgressReporter>(
&self,
pkg_ref: &Self::Ref,
_options: &DownloadOptions<R>,
) -> Result<(PackageFS, Target), Self::DownloadError> {
let manifest = deser_manifest(&pkg_ref.path).await?;
Ok((
PackageFS::Copy(pkg_ref.path.clone(), manifest.target.kind()),
manifest.target,
))
}
}
/// Errors that can occur when using a path package source
pub mod errors {
use std::path::PathBuf;
use thiserror::Error;
/// Errors that can occur when refreshing the path package source
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum RefreshError {}
/// Errors that can occur when resolving a path package
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ResolveError {
/// Reading the manifest failed
#[error("error reading manifest")]
ManifestRead(#[from] crate::errors::ManifestReadError),
/// An error occurred getting all dependencies
#[error("failed to get all dependencies")]
AllDependencies(#[from] crate::manifest::errors::AllDependenciesError),
/// An index of the package was not found
#[error("index {0} not found in package {1}")]
IndexNotFound(String, PathBuf),
}
/// Errors that can occur when downloading a path package
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum DownloadError {
/// Reading the manifest failed
#[error("error reading manifest")]
ManifestRead(#[from] crate::errors::ManifestReadError),
}
}

View file

@ -0,0 +1,29 @@
use crate::{
manifest::DependencyType,
source::{path::PathPackageSource, DependencySpecifiers, PackageRef, PackageSources},
};
use serde::{Deserialize, Serialize};
use std::{collections::BTreeMap, path::PathBuf};
/// A path package reference
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
pub struct PathPackageRef {
/// The path of the package
pub path: PathBuf,
/// The dependencies of the package
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub dependencies: BTreeMap<String, (DependencySpecifiers, DependencyType)>,
}
impl PackageRef for PathPackageRef {
fn dependencies(&self) -> &BTreeMap<String, (DependencySpecifiers, DependencyType)> {
&self.dependencies
}
fn use_new_structure(&self) -> bool {
true
}
fn source(&self) -> PackageSources {
PackageSources::Path(PathPackageSource)
}
}

View file

@ -0,0 +1,17 @@
use crate::source::DependencySpecifier;
use serde::{Deserialize, Serialize};
use std::{fmt::Display, path::PathBuf};
/// The specifier for a path dependency
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
pub struct PathDependencySpecifier {
/// The path to the package
pub path: PathBuf,
}
impl DependencySpecifier for PathDependencySpecifier {}
impl Display for PathDependencySpecifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "path:{}", self.path.display())
}
}

View file

@ -18,6 +18,8 @@ pub enum PackageRefs {
Git(crate::source::git::pkg_ref::GitPackageRef),
/// A workspace package reference
Workspace(crate::source::workspace::pkg_ref::WorkspacePackageRef),
/// A path package reference
Path(crate::source::path::pkg_ref::PathPackageRef),
}
impl PackageRefs {
@ -40,6 +42,7 @@ impl PackageRef for PackageRefs {
PackageRefs::Wally(pkg_ref) => pkg_ref.dependencies(),
PackageRefs::Git(pkg_ref) => pkg_ref.dependencies(),
PackageRefs::Workspace(pkg_ref) => pkg_ref.dependencies(),
PackageRefs::Path(pkg_ref) => pkg_ref.dependencies(),
}
}
@ -50,6 +53,7 @@ impl PackageRef for PackageRefs {
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(),
PackageRefs::Path(pkg_ref) => pkg_ref.use_new_structure(),
}
}
@ -60,6 +64,7 @@ impl PackageRef for PackageRefs {
PackageRefs::Wally(pkg_ref) => pkg_ref.source(),
PackageRefs::Git(pkg_ref) => pkg_ref.source(),
PackageRefs::Workspace(pkg_ref) => pkg_ref.source(),
PackageRefs::Path(pkg_ref) => pkg_ref.source(),
}
}
}

View file

@ -15,6 +15,8 @@ pub enum DependencySpecifiers {
Git(crate::source::git::specifier::GitDependencySpecifier),
/// A workspace dependency specifier
Workspace(crate::source::workspace::specifier::WorkspaceDependencySpecifier),
/// A path dependency specifier
Path(crate::source::path::specifier::PathDependencySpecifier),
}
impl DependencySpecifier for DependencySpecifiers {}
@ -26,6 +28,7 @@ impl Display for DependencySpecifiers {
DependencySpecifiers::Wally(specifier) => write!(f, "{specifier}"),
DependencySpecifiers::Git(specifier) => write!(f, "{specifier}"),
DependencySpecifiers::Workspace(specifier) => write!(f, "{specifier}"),
DependencySpecifiers::Path(specifier) => write!(f, "{specifier}"),
}
}
}

View file

@ -110,6 +110,7 @@ impl PackageSource for WorkspacePackageSource {
}
DependencySpecifiers::Git(_) => {}
DependencySpecifiers::Workspace(_) => {}
DependencySpecifiers::Path(_) => {}
}
Ok((alias, (spec, ty)))