mirror of
https://github.com/pesde-pkg/pesde.git
synced 2024-12-12 11:00:36 +00:00
feat: ✨ multi-index + wally support
This commit is contained in:
parent
e021c5f408
commit
984dd2ed0f
27 changed files with 2244 additions and 794 deletions
473
Cargo.lock
generated
473
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
15
Cargo.toml
15
Cargo.toml
|
@ -10,6 +10,7 @@ include = ["src/**/*", "Cargo.toml", "Cargo.lock", "README.md", "LICENSE", "CHAN
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
bin = ["clap", "directories", "keyring", "anyhow", "ignore", "pretty_env_logger", "serde_json", "reqwest/json", "reqwest/multipart", "lune", "futures-executor", "indicatif", "auth-git2", "indicatif-log-bridge", "inquire", "once_cell"]
|
bin = ["clap", "directories", "keyring", "anyhow", "ignore", "pretty_env_logger", "serde_json", "reqwest/json", "reqwest/multipart", "lune", "futures-executor", "indicatif", "auth-git2", "indicatif-log-bridge", "inquire", "once_cell"]
|
||||||
|
wally = ["toml", "zip", "serde_json"]
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "pesde"
|
name = "pesde"
|
||||||
|
@ -18,11 +19,10 @@ required-features = ["bin"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1.0.197", features = ["derive"] }
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
serde_yaml = "0.9.32"
|
serde_yaml = "0.9.33"
|
||||||
toml = "0.8.11"
|
git2 = "0.18.3"
|
||||||
git2 = "0.18.2"
|
|
||||||
semver = { version = "1.0.22", features = ["serde"] }
|
semver = { version = "1.0.22", features = ["serde"] }
|
||||||
reqwest = { version = "0.11.26", default-features = false, features = ["rustls-tls", "blocking"] }
|
reqwest = { version = "0.12.1", default-features = false, features = ["rustls-tls", "blocking"] }
|
||||||
tar = "0.4.40"
|
tar = "0.4.40"
|
||||||
flate2 = "1.0.28"
|
flate2 = "1.0.28"
|
||||||
pathdiff = "0.2.1"
|
pathdiff = "0.2.1"
|
||||||
|
@ -31,6 +31,11 @@ log = "0.4.21"
|
||||||
thiserror = "1.0.58"
|
thiserror = "1.0.58"
|
||||||
threadpool = "1.8.1"
|
threadpool = "1.8.1"
|
||||||
full_moon = { version = "0.19.0", features = ["stacker", "roblox"] }
|
full_moon = { version = "0.19.0", features = ["stacker", "roblox"] }
|
||||||
|
url = { version = "2.5.0", features = ["serde"] }
|
||||||
|
cfg-if = "1.0.0"
|
||||||
|
|
||||||
|
toml = { version = "0.8.12", optional = true }
|
||||||
|
zip = { version = "0.6.6", optional = true }
|
||||||
|
|
||||||
# chrono-lc breaks because of https://github.com/chronotope/chrono/compare/v0.4.34...v0.4.35#diff-67de5678fb5c14378bbff7ecf7f8bfab17cc223c4726f8da3afca183a4e59543
|
# chrono-lc breaks because of https://github.com/chronotope/chrono/compare/v0.4.34...v0.4.35#diff-67de5678fb5c14378bbff7ecf7f8bfab17cc223c4726f8da3afca183a4e59543
|
||||||
chrono = { version = "=0.4.34", features = ["serde"] }
|
chrono = { version = "=0.4.34", features = ["serde"] }
|
||||||
|
@ -47,7 +52,7 @@ futures-executor = { version = "0.3.30", optional = true }
|
||||||
indicatif = { version = "0.17.8", optional = true }
|
indicatif = { version = "0.17.8", optional = true }
|
||||||
auth-git2 = { version = "0.5.4", optional = true }
|
auth-git2 = { version = "0.5.4", optional = true }
|
||||||
indicatif-log-bridge = { version = "0.2.2", optional = true }
|
indicatif-log-bridge = { version = "0.2.2", optional = true }
|
||||||
inquire = { version = "0.7.1", optional = true }
|
inquire = { version = "0.7.3", optional = true }
|
||||||
once_cell = { version = "1.19.0", optional = true }
|
once_cell = { version = "1.19.0", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
|
@ -13,6 +13,7 @@ Currently, pesde is in a very early stage of development, but already supports t
|
||||||
- Re-exporting types
|
- Re-exporting types
|
||||||
- `bin` exports (ran with Lune)
|
- `bin` exports (ran with Lune)
|
||||||
- Patching packages
|
- Patching packages
|
||||||
|
- Downloading packages from Wally registries
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|
|
@ -11,17 +11,17 @@ actix-multipart = "0.6.1"
|
||||||
actix-multipart-derive = "0.6.1"
|
actix-multipart-derive = "0.6.1"
|
||||||
actix-governor = "0.5.0"
|
actix-governor = "0.5.0"
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
reqwest = { version = "0.11.24", features = ["json", "blocking"] }
|
reqwest = { version = "0.12.1", features = ["json", "blocking"] }
|
||||||
rusty-s3 = "0.5.0"
|
rusty-s3 = "0.5.0"
|
||||||
serde = { version = "1.0.197", features = ["derive"] }
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
serde_json = "1.0.114"
|
serde_json = "1.0.114"
|
||||||
serde_yaml = "0.9.32"
|
serde_yaml = "0.9.33"
|
||||||
flate2 = "1.0.28"
|
flate2 = "1.0.28"
|
||||||
tar = "0.4.40"
|
tar = "0.4.40"
|
||||||
pesde = { path = ".." }
|
pesde = { path = ".." }
|
||||||
semver = "1.0.22"
|
semver = "1.0.22"
|
||||||
git2 = "0.18.2"
|
git2 = "0.18.3"
|
||||||
thiserror = "1.0.57"
|
thiserror = "1.0.58"
|
||||||
tantivy = "0.21.1"
|
tantivy = "0.21.1"
|
||||||
log = "0.4.21"
|
log = "0.4.21"
|
||||||
pretty_env_logger = "0.5.0"
|
pretty_env_logger = "0.5.0"
|
||||||
|
|
|
@ -8,8 +8,9 @@ use tantivy::{doc, DateTime, Term};
|
||||||
use tar::Archive;
|
use tar::Archive;
|
||||||
|
|
||||||
use pesde::{
|
use pesde::{
|
||||||
dependencies::DependencySpecifier, index::Index, manifest::Manifest, package_name::PackageName,
|
dependencies::DependencySpecifier, index::Index, manifest::Manifest,
|
||||||
IGNORED_FOLDERS, MANIFEST_FILE_NAME,
|
package_name::StandardPackageName, project::DEFAULT_INDEX_NAME, IGNORED_FOLDERS,
|
||||||
|
MANIFEST_FILE_NAME,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{commit_signature, errors, AppState, UserId, S3_EXPIRY};
|
use crate::{commit_signature, errors, AppState, UserId, S3_EXPIRY};
|
||||||
|
@ -83,7 +84,7 @@ pub async fn create_package(
|
||||||
let mut index = app_state.index.lock().unwrap();
|
let mut index = app_state.index.lock().unwrap();
|
||||||
let config = index.config()?;
|
let config = index.config()?;
|
||||||
|
|
||||||
for (dependency, _) in manifest.dependencies().iter() {
|
for (dependency, _) in manifest.dependencies() {
|
||||||
match dependency {
|
match dependency {
|
||||||
DependencySpecifier::Git(_) => {
|
DependencySpecifier::Git(_) => {
|
||||||
if !config.git_allowed {
|
if !config.git_allowed {
|
||||||
|
@ -93,12 +94,24 @@ pub async fn create_package(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DependencySpecifier::Registry(registry) => {
|
DependencySpecifier::Registry(registry) => {
|
||||||
if index.package(®istry.name).unwrap().is_none() {
|
if index
|
||||||
|
.package(®istry.name.clone().into())
|
||||||
|
.unwrap()
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
return Ok(HttpResponse::BadRequest().json(errors::ErrorResponse {
|
return Ok(HttpResponse::BadRequest().json(errors::ErrorResponse {
|
||||||
error: format!("Dependency {} not found", registry.name),
|
error: format!("Dependency {} not found", registry.name),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if registry.index != DEFAULT_INDEX_NAME && !config.custom_registry_allowed {
|
||||||
|
return Ok(HttpResponse::BadRequest().json(errors::ErrorResponse {
|
||||||
|
error: "Custom registries are not allowed on this registry".to_string(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
#[allow(unreachable_patterns)]
|
||||||
|
_ => {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,12 +179,12 @@ pub async fn get_package_version(
|
||||||
) -> Result<impl Responder, errors::Errors> {
|
) -> Result<impl Responder, errors::Errors> {
|
||||||
let (scope, name, mut version) = path.into_inner();
|
let (scope, name, mut version) = path.into_inner();
|
||||||
|
|
||||||
let package_name = PackageName::new(&scope, &name)?;
|
let package_name = StandardPackageName::new(&scope, &name)?;
|
||||||
|
|
||||||
{
|
{
|
||||||
let index = app_state.index.lock().unwrap();
|
let index = app_state.index.lock().unwrap();
|
||||||
|
|
||||||
match index.package(&package_name)? {
|
match index.package(&package_name.clone().into())? {
|
||||||
Some(package) => {
|
Some(package) => {
|
||||||
if version == "latest" {
|
if version == "latest" {
|
||||||
version = package.last().map(|v| v.version.to_string()).unwrap();
|
version = package.last().map(|v| v.version.to_string()).unwrap();
|
||||||
|
@ -223,12 +236,12 @@ pub async fn get_package_versions(
|
||||||
) -> Result<impl Responder, errors::Errors> {
|
) -> Result<impl Responder, errors::Errors> {
|
||||||
let (scope, name) = path.into_inner();
|
let (scope, name) = path.into_inner();
|
||||||
|
|
||||||
let package_name = PackageName::new(&scope, &name)?;
|
let package_name = StandardPackageName::new(&scope, &name)?;
|
||||||
|
|
||||||
{
|
{
|
||||||
let index = app_state.index.lock().unwrap();
|
let index = app_state.index.lock().unwrap();
|
||||||
|
|
||||||
match index.package(&package_name)? {
|
match index.package(&package_name.into())? {
|
||||||
Some(package) => {
|
Some(package) => {
|
||||||
let versions = package
|
let versions = package
|
||||||
.iter()
|
.iter()
|
||||||
|
|
|
@ -4,7 +4,7 @@ use serde::Deserialize;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use tantivy::{query::AllQuery, DateTime, DocAddress, Order};
|
use tantivy::{query::AllQuery, DateTime, DocAddress, Order};
|
||||||
|
|
||||||
use pesde::{index::Index, package_name::PackageName};
|
use pesde::{index::Index, package_name::StandardPackageName};
|
||||||
|
|
||||||
use crate::{errors, AppState};
|
use crate::{errors, AppState};
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ pub async fn search_packages(
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(published_at, doc_address)| {
|
.map(|(published_at, doc_address)| {
|
||||||
let retrieved_doc = searcher.doc(doc_address).unwrap();
|
let retrieved_doc = searcher.doc(doc_address).unwrap();
|
||||||
let name: PackageName = retrieved_doc
|
let name: StandardPackageName = retrieved_doc
|
||||||
.get_first(name)
|
.get_first(name)
|
||||||
.and_then(|v| v.as_text())
|
.and_then(|v| v.as_text())
|
||||||
.and_then(|v| v.parse().ok())
|
.and_then(|v| v.parse().ok())
|
||||||
|
@ -63,7 +63,7 @@ pub async fn search_packages(
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let entry = index
|
let entry = index
|
||||||
.package(&name)
|
.package(&name.clone().into())
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.and_then(|v| v.into_iter().find(|v| v.version == version))
|
.and_then(|v| v.into_iter().find(|v| v.version == version))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use actix_web::{HttpResponse, ResponseError};
|
use actix_web::{HttpResponse, ResponseError};
|
||||||
use log::error;
|
use log::error;
|
||||||
|
use pesde::index::CreatePackageVersionError;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
@ -20,13 +21,13 @@ pub enum Errors {
|
||||||
Reqwest(#[from] reqwest::Error),
|
Reqwest(#[from] reqwest::Error),
|
||||||
|
|
||||||
#[error("package name invalid")]
|
#[error("package name invalid")]
|
||||||
PackageName(#[from] pesde::package_name::PackageNameValidationError),
|
PackageName(#[from] pesde::package_name::StandardPackageNameValidationError),
|
||||||
|
|
||||||
#[error("config error")]
|
#[error("config error")]
|
||||||
Config(#[from] pesde::index::ConfigError),
|
Config(#[from] pesde::index::ConfigError),
|
||||||
|
|
||||||
#[error("create package version error")]
|
#[error("create package version error")]
|
||||||
CreatePackageVersion(#[from] pesde::index::CreatePackageVersionError),
|
CreatePackageVersion(#[from] CreatePackageVersionError),
|
||||||
|
|
||||||
#[error("commit and push error")]
|
#[error("commit and push error")]
|
||||||
CommitAndPush(#[from] pesde::index::CommitAndPushError),
|
CommitAndPush(#[from] pesde::index::CommitAndPushError),
|
||||||
|
@ -43,11 +44,16 @@ impl ResponseError for Errors {
|
||||||
match self {
|
match self {
|
||||||
Errors::UserYaml(_) | Errors::PackageName(_) | Errors::QueryParser(_) => {}
|
Errors::UserYaml(_) | Errors::PackageName(_) | Errors::QueryParser(_) => {}
|
||||||
Errors::CreatePackageVersion(err) => match err {
|
Errors::CreatePackageVersion(err) => match err {
|
||||||
pesde::index::CreatePackageVersionError::MissingScopeOwnership => {
|
CreatePackageVersionError::MissingScopeOwnership => {
|
||||||
return HttpResponse::Unauthorized().json(ErrorResponse {
|
return HttpResponse::Unauthorized().json(ErrorResponse {
|
||||||
error: "You do not have permission to publish this scope".to_string(),
|
error: "You do not have permission to publish this scope".to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
CreatePackageVersionError::FromManifestIndexFileEntry(err) => {
|
||||||
|
return HttpResponse::BadRequest().json(ErrorResponse {
|
||||||
|
error: format!("Error in manifest: {err:?}"),
|
||||||
|
});
|
||||||
|
}
|
||||||
_ => error!("{err:?}"),
|
_ => error!("{err:?}"),
|
||||||
},
|
},
|
||||||
err => {
|
err => {
|
||||||
|
|
|
@ -18,8 +18,8 @@ use rusty_s3::{Bucket, Credentials, UrlStyle};
|
||||||
use tantivy::{doc, DateTime, IndexReader, IndexWriter};
|
use tantivy::{doc, DateTime, IndexReader, IndexWriter};
|
||||||
|
|
||||||
use pesde::{
|
use pesde::{
|
||||||
index::{GitIndex, IndexFile},
|
index::{GitIndex, Index, IndexFile},
|
||||||
package_name::PackageName,
|
package_name::StandardPackageName,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod endpoints;
|
mod endpoints;
|
||||||
|
@ -157,7 +157,7 @@ fn search_index(index: &GitIndex) -> (IndexReader, IndexWriter) {
|
||||||
|
|
||||||
let package = path.file_name().and_then(|v| v.to_str()).unwrap();
|
let package = path.file_name().and_then(|v| v.to_str()).unwrap();
|
||||||
|
|
||||||
let package_name = PackageName::new(scope, package).unwrap();
|
let package_name = StandardPackageName::new(scope, package).unwrap();
|
||||||
let entries: IndexFile =
|
let entries: IndexFile =
|
||||||
serde_yaml::from_slice(&std::fs::read(&path).unwrap()).unwrap();
|
serde_yaml::from_slice(&std::fs::read(&path).unwrap()).unwrap();
|
||||||
let entry = entries.last().unwrap().clone();
|
let entry = entries.last().unwrap().clone();
|
||||||
|
@ -216,7 +216,7 @@ fn main() -> std::io::Result<()> {
|
||||||
|
|
||||||
let index = GitIndex::new(
|
let index = GitIndex::new(
|
||||||
current_dir.join("cache"),
|
current_dir.join("cache"),
|
||||||
&get_env!("INDEX_REPO_URL"),
|
&get_env!("INDEX_REPO_URL", "p"),
|
||||||
Some(Box::new(|| {
|
Some(Box::new(|| {
|
||||||
Box::new(|_, _, _| {
|
Box::new(|_, _, _| {
|
||||||
let username = get_env!("GITHUB_USERNAME");
|
let username = get_env!("GITHUB_USERNAME");
|
||||||
|
@ -225,6 +225,7 @@ fn main() -> std::io::Result<()> {
|
||||||
Cred::userpass_plaintext(&username, &pat)
|
Cred::userpass_plaintext(&username, &pat)
|
||||||
})
|
})
|
||||||
})),
|
})),
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
index.refresh().expect("failed to refresh index");
|
index.refresh().expect("failed to refresh index");
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,15 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::cli::DEFAULT_INDEX_DATA;
|
||||||
use keyring::Entry;
|
use keyring::Entry;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::cli::INDEX_DIR;
|
struct EnvVarApiTokenSource;
|
||||||
|
|
||||||
pub trait ApiTokenSource: Send + Sync {
|
|
||||||
fn get_api_token(&self) -> anyhow::Result<Option<String>>;
|
|
||||||
fn set_api_token(&self, api_token: &str) -> anyhow::Result<()>;
|
|
||||||
fn delete_api_token(&self) -> anyhow::Result<()>;
|
|
||||||
fn persists(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct EnvVarApiTokenSource;
|
|
||||||
|
|
||||||
const API_TOKEN_ENV_VAR: &str = "PESDE_API_TOKEN";
|
const API_TOKEN_ENV_VAR: &str = "PESDE_API_TOKEN";
|
||||||
|
|
||||||
impl ApiTokenSource for EnvVarApiTokenSource {
|
impl EnvVarApiTokenSource {
|
||||||
fn get_api_token(&self) -> anyhow::Result<Option<String>> {
|
fn get_api_token(&self) -> anyhow::Result<Option<String>> {
|
||||||
match std::env::var(API_TOKEN_ENV_VAR) {
|
match std::env::var(API_TOKEN_ENV_VAR) {
|
||||||
Ok(token) => Ok(Some(token)),
|
Ok(token) => Ok(Some(token)),
|
||||||
|
@ -27,51 +17,10 @@ impl ApiTokenSource for EnvVarApiTokenSource {
|
||||||
Err(e) => Err(e.into()),
|
Err(e) => Err(e.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't need to implement set_api_token or delete_api_token
|
|
||||||
fn set_api_token(&self, _api_token: &str) -> anyhow::Result<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete_api_token(&self) -> anyhow::Result<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn persists(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static KEYRING_ENTRY: Lazy<Entry> =
|
static AUTH_FILE_PATH: Lazy<PathBuf> =
|
||||||
Lazy::new(|| Entry::new(env!("CARGO_BIN_NAME"), "api_token").unwrap());
|
Lazy::new(|| DEFAULT_INDEX_DATA.0.parent().unwrap().join("auth.yaml"));
|
||||||
|
|
||||||
pub struct KeyringApiTokenSource;
|
|
||||||
|
|
||||||
impl ApiTokenSource for KeyringApiTokenSource {
|
|
||||||
fn get_api_token(&self) -> anyhow::Result<Option<String>> {
|
|
||||||
match KEYRING_ENTRY.get_password() {
|
|
||||||
Ok(api_token) => Ok(Some(api_token)),
|
|
||||||
Err(err) => match err {
|
|
||||||
keyring::Error::NoEntry | keyring::Error::PlatformFailure(_) => Ok(None),
|
|
||||||
_ => Err(err.into()),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_api_token(&self, api_token: &str) -> anyhow::Result<()> {
|
|
||||||
KEYRING_ENTRY.set_password(api_token)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete_api_token(&self) -> anyhow::Result<()> {
|
|
||||||
KEYRING_ENTRY.delete_password()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static AUTH_FILE_PATH: Lazy<PathBuf> = Lazy::new(|| INDEX_DIR.join("auth.yaml"));
|
|
||||||
static AUTH_FILE: Lazy<AuthFile> =
|
static AUTH_FILE: Lazy<AuthFile> =
|
||||||
Lazy::new(
|
Lazy::new(
|
||||||
|| match std::fs::read_to_string(AUTH_FILE_PATH.to_path_buf()) {
|
|| match std::fs::read_to_string(AUTH_FILE_PATH.to_path_buf()) {
|
||||||
|
@ -87,9 +36,9 @@ struct AuthFile {
|
||||||
api_token: Option<String>,
|
api_token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ConfigFileApiTokenSource;
|
struct ConfigFileApiTokenSource;
|
||||||
|
|
||||||
impl ApiTokenSource for ConfigFileApiTokenSource {
|
impl ConfigFileApiTokenSource {
|
||||||
fn get_api_token(&self) -> anyhow::Result<Option<String>> {
|
fn get_api_token(&self) -> anyhow::Result<Option<String>> {
|
||||||
Ok(AUTH_FILE.api_token.clone())
|
Ok(AUTH_FILE.api_token.clone())
|
||||||
}
|
}
|
||||||
|
@ -120,11 +69,77 @@ impl ApiTokenSource for ConfigFileApiTokenSource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub static API_TOKEN_SOURCE: Lazy<Box<dyn ApiTokenSource>> = Lazy::new(|| {
|
static KEYRING_ENTRY: Lazy<Entry> =
|
||||||
let sources: Vec<Box<dyn ApiTokenSource>> = vec![
|
Lazy::new(|| Entry::new(env!("CARGO_PKG_NAME"), "api_token").unwrap());
|
||||||
Box::new(EnvVarApiTokenSource),
|
|
||||||
Box::new(KeyringApiTokenSource),
|
struct KeyringApiTokenSource;
|
||||||
Box::new(ConfigFileApiTokenSource),
|
|
||||||
|
impl KeyringApiTokenSource {
|
||||||
|
fn get_api_token(&self) -> anyhow::Result<Option<String>> {
|
||||||
|
match KEYRING_ENTRY.get_password() {
|
||||||
|
Ok(api_token) => Ok(Some(api_token)),
|
||||||
|
Err(err) => match err {
|
||||||
|
keyring::Error::NoEntry | keyring::Error::PlatformFailure(_) => Ok(None),
|
||||||
|
_ => Err(err.into()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_api_token(&self, api_token: &str) -> anyhow::Result<()> {
|
||||||
|
KEYRING_ENTRY.set_password(api_token)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_api_token(&self) -> anyhow::Result<()> {
|
||||||
|
KEYRING_ENTRY.delete_password()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ApiTokenSource {
|
||||||
|
EnvVar,
|
||||||
|
ConfigFile,
|
||||||
|
Keyring,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApiTokenSource {
|
||||||
|
pub fn get_api_token(&self) -> anyhow::Result<Option<String>> {
|
||||||
|
match self {
|
||||||
|
ApiTokenSource::EnvVar => EnvVarApiTokenSource.get_api_token(),
|
||||||
|
ApiTokenSource::ConfigFile => ConfigFileApiTokenSource.get_api_token(),
|
||||||
|
ApiTokenSource::Keyring => KeyringApiTokenSource.get_api_token(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_api_token(&self, api_token: &str) -> anyhow::Result<()> {
|
||||||
|
match self {
|
||||||
|
ApiTokenSource::EnvVar => Ok(()),
|
||||||
|
ApiTokenSource::ConfigFile => ConfigFileApiTokenSource.set_api_token(api_token),
|
||||||
|
ApiTokenSource::Keyring => KeyringApiTokenSource.set_api_token(api_token),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_api_token(&self) -> anyhow::Result<()> {
|
||||||
|
match self {
|
||||||
|
ApiTokenSource::EnvVar => Ok(()),
|
||||||
|
ApiTokenSource::ConfigFile => ConfigFileApiTokenSource.delete_api_token(),
|
||||||
|
ApiTokenSource::Keyring => KeyringApiTokenSource.delete_api_token(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn persists(&self) -> bool {
|
||||||
|
!matches!(self, ApiTokenSource::EnvVar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub static API_TOKEN_SOURCE: Lazy<ApiTokenSource> = Lazy::new(|| {
|
||||||
|
let sources: [ApiTokenSource; 3] = [
|
||||||
|
ApiTokenSource::EnvVar,
|
||||||
|
ApiTokenSource::ConfigFile,
|
||||||
|
ApiTokenSource::Keyring,
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut valid_sources = vec![];
|
let mut valid_sources = vec![];
|
||||||
|
|
|
@ -2,7 +2,7 @@ use clap::Subcommand;
|
||||||
use pesde::index::Index;
|
use pesde::index::Index;
|
||||||
use reqwest::{header::AUTHORIZATION, Url};
|
use reqwest::{header::AUTHORIZATION, Url};
|
||||||
|
|
||||||
use crate::cli::{api_token::API_TOKEN_SOURCE, send_request, INDEX, REQWEST_CLIENT};
|
use crate::cli::{api_token::API_TOKEN_SOURCE, send_request, DEFAULT_INDEX, REQWEST_CLIENT};
|
||||||
|
|
||||||
#[derive(Subcommand, Clone)]
|
#[derive(Subcommand, Clone)]
|
||||||
pub enum AuthCommand {
|
pub enum AuthCommand {
|
||||||
|
@ -15,7 +15,7 @@ pub enum AuthCommand {
|
||||||
pub fn auth_command(cmd: AuthCommand) -> anyhow::Result<()> {
|
pub fn auth_command(cmd: AuthCommand) -> anyhow::Result<()> {
|
||||||
match cmd {
|
match cmd {
|
||||||
AuthCommand::Login => {
|
AuthCommand::Login => {
|
||||||
let github_oauth_client_id = INDEX.config()?.github_oauth_client_id;
|
let github_oauth_client_id = DEFAULT_INDEX.config()?.github_oauth_client_id;
|
||||||
|
|
||||||
let response = send_request(REQWEST_CLIENT.post(Url::parse_with_params(
|
let response = send_request(REQWEST_CLIENT.post(Url::parse_with_params(
|
||||||
"https://github.com/login/device/code",
|
"https://github.com/login/device/code",
|
||||||
|
|
|
@ -6,15 +6,6 @@ use crate::{cli::CLI_CONFIG, CliConfig};
|
||||||
|
|
||||||
#[derive(Subcommand, Clone)]
|
#[derive(Subcommand, Clone)]
|
||||||
pub enum ConfigCommand {
|
pub enum ConfigCommand {
|
||||||
/// Sets the index repository URL
|
|
||||||
SetIndexRepo {
|
|
||||||
/// The URL of the index repository
|
|
||||||
#[clap(value_name = "URL")]
|
|
||||||
url: String,
|
|
||||||
},
|
|
||||||
/// Gets the index repository URL
|
|
||||||
GetIndexRepo,
|
|
||||||
|
|
||||||
/// Sets the cache directory
|
/// Sets the cache directory
|
||||||
SetCacheDir {
|
SetCacheDir {
|
||||||
/// The directory to use as the cache directory
|
/// The directory to use as the cache directory
|
||||||
|
@ -27,26 +18,9 @@ pub enum ConfigCommand {
|
||||||
|
|
||||||
pub fn config_command(cmd: ConfigCommand) -> anyhow::Result<()> {
|
pub fn config_command(cmd: ConfigCommand) -> anyhow::Result<()> {
|
||||||
match cmd {
|
match cmd {
|
||||||
ConfigCommand::SetIndexRepo { url } => {
|
|
||||||
let cli_config = CliConfig {
|
|
||||||
index_repo_url: url.clone(),
|
|
||||||
..CLI_CONFIG.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
cli_config.write()?;
|
|
||||||
|
|
||||||
println!("index repository url set to: `{url}`");
|
|
||||||
}
|
|
||||||
ConfigCommand::GetIndexRepo => {
|
|
||||||
println!(
|
|
||||||
"current index repository url: `{}`",
|
|
||||||
CLI_CONFIG.index_repo_url
|
|
||||||
);
|
|
||||||
}
|
|
||||||
ConfigCommand::SetCacheDir { directory } => {
|
ConfigCommand::SetCacheDir { directory } => {
|
||||||
let cli_config = CliConfig {
|
let cli_config = CliConfig {
|
||||||
cache_dir: directory,
|
cache_dir: directory,
|
||||||
..CLI_CONFIG.clone()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
cli_config.write()?;
|
cli_config.write()?;
|
||||||
|
|
|
@ -6,7 +6,12 @@ use indicatif::MultiProgress;
|
||||||
use indicatif_log_bridge::LogWrapper;
|
use indicatif_log_bridge::LogWrapper;
|
||||||
use log::error;
|
use log::error;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use pesde::{index::GitIndex, manifest::Realm, package_name::PackageName};
|
use pesde::{
|
||||||
|
index::{GitIndex, Index},
|
||||||
|
manifest::{Manifest, Realm},
|
||||||
|
package_name::{PackageName, StandardPackageName},
|
||||||
|
project::DEFAULT_INDEX_NAME,
|
||||||
|
};
|
||||||
use pretty_env_logger::env_logger::Env;
|
use pretty_env_logger::env_logger::Env;
|
||||||
use reqwest::{
|
use reqwest::{
|
||||||
blocking::{RequestBuilder, Response},
|
blocking::{RequestBuilder, Response},
|
||||||
|
@ -84,7 +89,7 @@ pub enum Command {
|
||||||
Run {
|
Run {
|
||||||
/// The package to run
|
/// The package to run
|
||||||
#[clap(value_name = "PACKAGE")]
|
#[clap(value_name = "PACKAGE")]
|
||||||
package: PackageName,
|
package: StandardPackageName,
|
||||||
|
|
||||||
/// The arguments to pass to the package
|
/// The arguments to pass to the package
|
||||||
#[clap(last = true)]
|
#[clap(last = true)]
|
||||||
|
@ -102,6 +107,7 @@ pub enum Command {
|
||||||
Publish,
|
Publish,
|
||||||
|
|
||||||
/// Converts a `wally.toml` file to a `pesde.yaml` file
|
/// Converts a `wally.toml` file to a `pesde.yaml` file
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
Convert,
|
Convert,
|
||||||
|
|
||||||
/// Begins a new patch
|
/// Begins a new patch
|
||||||
|
@ -141,21 +147,11 @@ pub struct Cli {
|
||||||
pub directory: Option<PathBuf>,
|
pub directory: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||||
pub struct CliConfig {
|
pub struct CliConfig {
|
||||||
pub index_repo_url: String,
|
|
||||||
pub cache_dir: Option<PathBuf>,
|
pub cache_dir: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for CliConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
index_repo_url: "https://github.com/daimond113/pesde-index".to_string(),
|
|
||||||
cache_dir: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CliConfig {
|
impl CliConfig {
|
||||||
pub fn cache_dir(&self) -> PathBuf {
|
pub fn cache_dir(&self) -> PathBuf {
|
||||||
self.cache_dir
|
self.cache_dir
|
||||||
|
@ -201,40 +197,12 @@ pub fn send_request(request_builder: RequestBuilder) -> anyhow::Result<Response>
|
||||||
pub static CLI: Lazy<Cli> = Lazy::new(Cli::parse);
|
pub static CLI: Lazy<Cli> = Lazy::new(Cli::parse);
|
||||||
|
|
||||||
pub static DIRS: Lazy<ProjectDirs> = Lazy::new(|| {
|
pub static DIRS: Lazy<ProjectDirs> = Lazy::new(|| {
|
||||||
ProjectDirs::from("com", env!("CARGO_BIN_NAME"), env!("CARGO_BIN_NAME"))
|
ProjectDirs::from("com", env!("CARGO_PKG_NAME"), env!("CARGO_BIN_NAME"))
|
||||||
.expect("couldn't get home directory")
|
.expect("couldn't get home directory")
|
||||||
});
|
});
|
||||||
|
|
||||||
pub static CLI_CONFIG: Lazy<CliConfig> = Lazy::new(|| CliConfig::open().unwrap());
|
pub static CLI_CONFIG: Lazy<CliConfig> = Lazy::new(|| CliConfig::open().unwrap());
|
||||||
|
|
||||||
pub static INDEX_DIR: Lazy<PathBuf> = Lazy::new(|| {
|
|
||||||
let mut hasher = DefaultHasher::new();
|
|
||||||
CLI_CONFIG.index_repo_url.hash(&mut hasher);
|
|
||||||
let hash = hasher.finish().to_string();
|
|
||||||
|
|
||||||
CLI_CONFIG.cache_dir().join("indices").join(hash)
|
|
||||||
});
|
|
||||||
|
|
||||||
pub static INDEX: Lazy<GitIndex> = Lazy::new(|| {
|
|
||||||
let index = GitIndex::new(
|
|
||||||
INDEX_DIR.join("index"),
|
|
||||||
&CLI_CONFIG.index_repo_url,
|
|
||||||
Some(Box::new(|| {
|
|
||||||
Box::new(|a, b, c| {
|
|
||||||
let git_authenticator = GitAuthenticator::new();
|
|
||||||
let config = git2::Config::open_default().unwrap();
|
|
||||||
let mut cred = git_authenticator.credentials(&config);
|
|
||||||
|
|
||||||
cred(a, b, c)
|
|
||||||
})
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
index.refresh().unwrap();
|
|
||||||
|
|
||||||
index
|
|
||||||
});
|
|
||||||
|
|
||||||
pub static CWD: Lazy<PathBuf> = Lazy::new(|| {
|
pub static CWD: Lazy<PathBuf> = Lazy::new(|| {
|
||||||
CLI.directory
|
CLI.directory
|
||||||
.clone()
|
.clone()
|
||||||
|
@ -275,3 +243,50 @@ pub static MULTI: Lazy<MultiProgress> = Lazy::new(|| {
|
||||||
|
|
||||||
multi
|
multi
|
||||||
});
|
});
|
||||||
|
|
||||||
|
pub const DEFAULT_INDEX_URL: &str = "https://github.com/daimond113/pesde-index";
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
pub const DEFAULT_WALLY_INDEX_URL: &str = "https://github.com/UpliftGames/wally-index";
|
||||||
|
|
||||||
|
pub fn index_dir(url: &str) -> PathBuf {
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
url.hash(&mut hasher);
|
||||||
|
let hash = hasher.finish().to_string();
|
||||||
|
|
||||||
|
CLI_CONFIG
|
||||||
|
.cache_dir()
|
||||||
|
.join("indices")
|
||||||
|
.join(hash)
|
||||||
|
.join("index")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clone_index(url: &str) -> GitIndex {
|
||||||
|
let index = GitIndex::new(
|
||||||
|
index_dir(url),
|
||||||
|
&url.parse().unwrap(),
|
||||||
|
Some(Box::new(|| {
|
||||||
|
Box::new(|a, b, c| {
|
||||||
|
let git_authenticator = GitAuthenticator::new();
|
||||||
|
let config = git2::Config::open_default().unwrap();
|
||||||
|
let mut cred = git_authenticator.credentials(&config);
|
||||||
|
|
||||||
|
cred(a, b, c)
|
||||||
|
})
|
||||||
|
})),
|
||||||
|
API_TOKEN_SOURCE.get_api_token().unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
index.refresh().unwrap();
|
||||||
|
|
||||||
|
index
|
||||||
|
}
|
||||||
|
|
||||||
|
pub static DEFAULT_INDEX_DATA: Lazy<(PathBuf, String)> = Lazy::new(|| {
|
||||||
|
let manifest = Manifest::from_path(CWD.to_path_buf())
|
||||||
|
.map(|m| m.indices.get(DEFAULT_INDEX_NAME).unwrap().clone());
|
||||||
|
let url = &manifest.unwrap_or(DEFAULT_INDEX_URL.to_string());
|
||||||
|
|
||||||
|
(index_dir(url), url.clone())
|
||||||
|
});
|
||||||
|
|
||||||
|
pub static DEFAULT_INDEX: Lazy<GitIndex> = Lazy::new(|| clone_index(&DEFAULT_INDEX_DATA.1));
|
||||||
|
|
150
src/cli/root.rs
150
src/cli/root.rs
|
@ -1,4 +1,7 @@
|
||||||
|
use cfg_if::cfg_if;
|
||||||
|
use chrono::Utc;
|
||||||
use std::{
|
use std::{
|
||||||
|
collections::{BTreeMap, HashMap},
|
||||||
fs::{create_dir_all, read, remove_dir_all, write, File},
|
fs::{create_dir_all, read, remove_dir_all, write, File},
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
|
@ -18,19 +21,19 @@ use tar::Builder as TarBuilder;
|
||||||
|
|
||||||
use pesde::{
|
use pesde::{
|
||||||
dependencies::{registry::RegistryDependencySpecifier, DependencySpecifier, PackageRef},
|
dependencies::{registry::RegistryDependencySpecifier, DependencySpecifier, PackageRef},
|
||||||
index::{GitIndex, Index},
|
index::Index,
|
||||||
manifest::{Manifest, PathStyle, Realm},
|
manifest::{Manifest, PathStyle, Realm},
|
||||||
multithread::MultithreadedJob,
|
multithread::MultithreadedJob,
|
||||||
package_name::PackageName,
|
package_name::{PackageName, StandardPackageName},
|
||||||
patches::{create_patch, setup_patches_repo},
|
patches::{create_patch, setup_patches_repo},
|
||||||
project::{InstallOptions, Project},
|
project::{InstallOptions, Project, DEFAULT_INDEX_NAME},
|
||||||
DEV_PACKAGES_FOLDER, IGNORED_FOLDERS, MANIFEST_FILE_NAME, PACKAGES_FOLDER, PATCHES_FOLDER,
|
DEV_PACKAGES_FOLDER, IGNORED_FOLDERS, MANIFEST_FILE_NAME, PACKAGES_FOLDER, PATCHES_FOLDER,
|
||||||
SERVER_PACKAGES_FOLDER,
|
SERVER_PACKAGES_FOLDER,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::cli::{
|
use crate::cli::{
|
||||||
api_token::API_TOKEN_SOURCE, send_request, Command, CLI_CONFIG, CWD, DIRS, INDEX, MULTI,
|
clone_index, send_request, Command, CLI_CONFIG, CWD, DEFAULT_INDEX, DEFAULT_INDEX_URL, DIRS,
|
||||||
REQWEST_CLIENT,
|
MULTI, REQWEST_CLIENT,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const MAX_ARCHIVE_SIZE: usize = 4 * 1024 * 1024;
|
pub const MAX_ARCHIVE_SIZE: usize = 4 * 1024 * 1024;
|
||||||
|
@ -71,12 +74,19 @@ macro_rules! none_if_empty {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn root_command(cmd: Command) -> anyhow::Result<()> {
|
pub fn root_command(cmd: Command) -> anyhow::Result<()> {
|
||||||
let project: Lazy<Project<GitIndex>> = Lazy::new(|| {
|
let mut project: Lazy<Project> = Lazy::new(|| {
|
||||||
Project::from_path(
|
let manifest = Manifest::from_path(CWD.to_path_buf()).unwrap();
|
||||||
|
let indices = manifest
|
||||||
|
.indices
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| (k, Box::new(clone_index(&v)) as Box<dyn Index>));
|
||||||
|
|
||||||
|
Project::new(
|
||||||
CWD.to_path_buf(),
|
CWD.to_path_buf(),
|
||||||
CLI_CONFIG.cache_dir(),
|
CLI_CONFIG.cache_dir(),
|
||||||
INDEX.clone(),
|
HashMap::from_iter(indices),
|
||||||
API_TOKEN_SOURCE.get_api_token().ok().flatten(),
|
manifest,
|
||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
});
|
});
|
||||||
|
@ -93,9 +103,10 @@ pub fn root_command(cmd: Command) -> anyhow::Result<()> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let resolved_versions_map = project.manifest().dependency_tree(&project, locked)?;
|
let manifest = project.manifest().clone();
|
||||||
|
let resolved_versions_map = manifest.dependency_tree(&mut project, locked)?;
|
||||||
|
|
||||||
let download_job = project.download(&resolved_versions_map)?;
|
let download_job = project.download(resolved_versions_map.clone())?;
|
||||||
|
|
||||||
multithreaded_bar(
|
multithreaded_bar(
|
||||||
download_job,
|
download_job,
|
||||||
|
@ -103,6 +114,8 @@ pub fn root_command(cmd: Command) -> anyhow::Result<()> {
|
||||||
"Downloading packages".to_string(),
|
"Downloading packages".to_string(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
let project = Lazy::force_mut(&mut project);
|
||||||
|
|
||||||
project.install(
|
project.install(
|
||||||
InstallOptions::new()
|
InstallOptions::new()
|
||||||
.locked(locked)
|
.locked(locked)
|
||||||
|
@ -116,7 +129,7 @@ pub fn root_command(cmd: Command) -> anyhow::Result<()> {
|
||||||
.ok_or(anyhow::anyhow!("lockfile not found"))?;
|
.ok_or(anyhow::anyhow!("lockfile not found"))?;
|
||||||
|
|
||||||
let (_, resolved_pkg) = lockfile
|
let (_, resolved_pkg) = lockfile
|
||||||
.get(&package)
|
.get(&package.into())
|
||||||
.and_then(|versions| versions.iter().find(|(_, pkg_ref)| pkg_ref.is_root))
|
.and_then(|versions| versions.iter().find(|(_, pkg_ref)| pkg_ref.is_root))
|
||||||
.ok_or(anyhow::anyhow!(
|
.ok_or(anyhow::anyhow!(
|
||||||
"package not found in lockfile (or isn't root)"
|
"package not found in lockfile (or isn't root)"
|
||||||
|
@ -143,7 +156,7 @@ pub fn root_command(cmd: Command) -> anyhow::Result<()> {
|
||||||
))?;
|
))?;
|
||||||
}
|
}
|
||||||
Command::Search { query } => {
|
Command::Search { query } => {
|
||||||
let config = INDEX.config()?;
|
let config = DEFAULT_INDEX.config()?;
|
||||||
let api_url = config.api();
|
let api_url = config.api();
|
||||||
|
|
||||||
let response = send_request(REQWEST_CLIENT.get(Url::parse_with_params(
|
let response = send_request(REQWEST_CLIENT.get(Url::parse_with_params(
|
||||||
|
@ -220,11 +233,13 @@ pub fn root_command(cmd: Command) -> anyhow::Result<()> {
|
||||||
.file_name("tarball.tar.gz")
|
.file_name("tarball.tar.gz")
|
||||||
.mime_str("application/gzip")?;
|
.mime_str("application/gzip")?;
|
||||||
|
|
||||||
|
let index = project.indices().get(DEFAULT_INDEX_NAME).unwrap();
|
||||||
|
|
||||||
let mut request = REQWEST_CLIENT
|
let mut request = REQWEST_CLIENT
|
||||||
.post(format!("{}/v0/packages", project.index().config()?.api()))
|
.post(format!("{}/v0/packages", index.config()?.api()))
|
||||||
.multipart(reqwest::blocking::multipart::Form::new().part("tarball", part));
|
.multipart(reqwest::blocking::multipart::Form::new().part("tarball", part));
|
||||||
|
|
||||||
if let Some(token) = project.registry_auth_token() {
|
if let Some(token) = index.registry_auth_token() {
|
||||||
request = request.header(AUTHORIZATION, format!("Bearer {token}"));
|
request = request.header(AUTHORIZATION, format!("Bearer {token}"));
|
||||||
} else {
|
} else {
|
||||||
request = request.header(AUTHORIZATION, "");
|
request = request.header(AUTHORIZATION, "");
|
||||||
|
@ -242,11 +257,11 @@ pub fn root_command(cmd: Command) -> anyhow::Result<()> {
|
||||||
.and_then(|versions| versions.get(&package.1))
|
.and_then(|versions| versions.get(&package.1))
|
||||||
.ok_or(anyhow::anyhow!("package not found in lockfile"))?;
|
.ok_or(anyhow::anyhow!("package not found in lockfile"))?;
|
||||||
|
|
||||||
let dir = DIRS.data_dir().join("patches").join(format!(
|
let dir = DIRS
|
||||||
"{}_{}",
|
.data_dir()
|
||||||
package.0.escaped(),
|
.join("patches")
|
||||||
package.1
|
.join(package.0.escaped())
|
||||||
));
|
.join(Utc::now().timestamp().to_string());
|
||||||
|
|
||||||
if dir.exists() {
|
if dir.exists() {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
|
@ -257,8 +272,20 @@ pub fn root_command(cmd: Command) -> anyhow::Result<()> {
|
||||||
|
|
||||||
create_dir_all(&dir)?;
|
create_dir_all(&dir)?;
|
||||||
|
|
||||||
resolved_pkg.pkg_ref.download(&project, &dir)?;
|
let project = Lazy::force_mut(&mut project);
|
||||||
match resolved_pkg.pkg_ref {
|
let url = resolved_pkg.pkg_ref.resolve_url(project)?;
|
||||||
|
|
||||||
|
let index = project.indices().get(DEFAULT_INDEX_NAME).unwrap();
|
||||||
|
|
||||||
|
resolved_pkg.pkg_ref.download(
|
||||||
|
&REQWEST_CLIENT,
|
||||||
|
index.registry_auth_token().map(|t| t.to_string()),
|
||||||
|
url.as_ref(),
|
||||||
|
index.credentials_fn().cloned(),
|
||||||
|
&dir,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
match &resolved_pkg.pkg_ref {
|
||||||
PackageRef::Git(_) => {}
|
PackageRef::Git(_) => {}
|
||||||
_ => {
|
_ => {
|
||||||
setup_patches_repo(&dir)?;
|
setup_patches_repo(&dir)?;
|
||||||
|
@ -268,13 +295,17 @@ pub fn root_command(cmd: Command) -> anyhow::Result<()> {
|
||||||
println!("done! modify the files in {} and run `{} patch-commit <DIRECTORY>` to commit the changes", dir.display(), env!("CARGO_BIN_NAME"));
|
println!("done! modify the files in {} and run `{} patch-commit <DIRECTORY>` to commit the changes", dir.display(), env!("CARGO_BIN_NAME"));
|
||||||
}
|
}
|
||||||
Command::PatchCommit { dir } => {
|
Command::PatchCommit { dir } => {
|
||||||
let manifest = Manifest::from_path(&dir)?;
|
let name = dir
|
||||||
let patch_path = project.path().join(PATCHES_FOLDER).join(format!(
|
.parent()
|
||||||
"{}@{}.patch",
|
.and_then(|p| p.file_name())
|
||||||
manifest.name.escaped(),
|
.and_then(|f| f.to_str())
|
||||||
manifest.version
|
.unwrap();
|
||||||
));
|
|
||||||
|
|
||||||
|
let manifest = Manifest::from_path(&dir)?;
|
||||||
|
let patch_path = project.path().join(PATCHES_FOLDER);
|
||||||
|
create_dir_all(&patch_path)?;
|
||||||
|
|
||||||
|
let patch_path = patch_path.join(format!("{name}@{}.patch", manifest.version));
|
||||||
if patch_path.exists() {
|
if patch_path.exists() {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"patch already exists. remove the file {} to create a new patch",
|
"patch already exists. remove the file {} to create a new patch",
|
||||||
|
@ -304,7 +335,7 @@ pub fn root_command(cmd: Command) -> anyhow::Result<()> {
|
||||||
|
|
||||||
let mut name =
|
let mut name =
|
||||||
Text::new("What is the name of the package?").with_validator(|name: &str| {
|
Text::new("What is the name of the package?").with_validator(|name: &str| {
|
||||||
Ok(match PackageName::from_str(name) {
|
Ok(match StandardPackageName::from_str(name) {
|
||||||
Ok(_) => Validation::Valid,
|
Ok(_) => Validation::Valid,
|
||||||
Err(e) => Validation::Invalid(e.into()),
|
Err(e) => Validation::Invalid(e.into()),
|
||||||
})
|
})
|
||||||
|
@ -358,6 +389,10 @@ pub fn root_command(cmd: Command) -> anyhow::Result<()> {
|
||||||
path_style,
|
path_style,
|
||||||
private,
|
private,
|
||||||
realm: Some(realm),
|
realm: Some(realm),
|
||||||
|
indices: BTreeMap::from([(
|
||||||
|
DEFAULT_INDEX_NAME.to_string(),
|
||||||
|
DEFAULT_INDEX_URL.to_string(),
|
||||||
|
)]),
|
||||||
dependencies: Default::default(),
|
dependencies: Default::default(),
|
||||||
peer_dependencies: Default::default(),
|
peer_dependencies: Default::default(),
|
||||||
description: none_if_empty!(description),
|
description: none_if_empty!(description),
|
||||||
|
@ -375,11 +410,25 @@ pub fn root_command(cmd: Command) -> anyhow::Result<()> {
|
||||||
} => {
|
} => {
|
||||||
let mut manifest = project.manifest().clone();
|
let mut manifest = project.manifest().clone();
|
||||||
|
|
||||||
let specifier = DependencySpecifier::Registry(RegistryDependencySpecifier {
|
let specifier = match package.0 {
|
||||||
name: package.0,
|
PackageName::Standard(name) => {
|
||||||
version: package.1,
|
DependencySpecifier::Registry(RegistryDependencySpecifier {
|
||||||
realm,
|
name,
|
||||||
});
|
version: package.1,
|
||||||
|
realm,
|
||||||
|
index: DEFAULT_INDEX_NAME.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
PackageName::Wally(name) => DependencySpecifier::Wally(
|
||||||
|
pesde::dependencies::wally::WallyDependencySpecifier {
|
||||||
|
name,
|
||||||
|
version: package.1,
|
||||||
|
realm,
|
||||||
|
index_url: crate::cli::DEFAULT_WALLY_INDEX_URL.parse().unwrap(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
if peer {
|
if peer {
|
||||||
manifest.peer_dependencies.push(specifier);
|
manifest.peer_dependencies.push(specifier);
|
||||||
|
@ -398,9 +447,27 @@ pub fn root_command(cmd: Command) -> anyhow::Result<()> {
|
||||||
for dependencies in [&mut manifest.dependencies, &mut manifest.peer_dependencies] {
|
for dependencies in [&mut manifest.dependencies, &mut manifest.peer_dependencies] {
|
||||||
dependencies.retain(|d| {
|
dependencies.retain(|d| {
|
||||||
if let DependencySpecifier::Registry(registry) = d {
|
if let DependencySpecifier::Registry(registry) = d {
|
||||||
registry.name != package
|
match &package {
|
||||||
|
PackageName::Standard(name) => ®istry.name != name,
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
PackageName::Wally(_) => true,
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
true
|
cfg_if! {
|
||||||
|
if #[cfg(feature = "wally")] {
|
||||||
|
#[allow(clippy::collapsible_else_if)]
|
||||||
|
if let DependencySpecifier::Wally(wally) = d {
|
||||||
|
match &package {
|
||||||
|
PackageName::Standard(_) => true,
|
||||||
|
PackageName::Wally(name) => &wally.name != name,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -411,8 +478,10 @@ pub fn root_command(cmd: Command) -> anyhow::Result<()> {
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
Command::Outdated => {
|
Command::Outdated => {
|
||||||
let manifest = project.manifest();
|
let project = Lazy::force_mut(&mut project);
|
||||||
let dependency_tree = manifest.dependency_tree(&project, false)?;
|
|
||||||
|
let manifest = project.manifest().clone();
|
||||||
|
let dependency_tree = manifest.dependency_tree(project, false)?;
|
||||||
|
|
||||||
for (name, versions) in dependency_tree {
|
for (name, versions) in dependency_tree {
|
||||||
for (version, resolved_pkg) in versions {
|
for (version, resolved_pkg) in versions {
|
||||||
|
@ -420,10 +489,10 @@ pub fn root_command(cmd: Command) -> anyhow::Result<()> {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let PackageRef::Registry(registry) = resolved_pkg.pkg_ref {
|
if let PackageRef::Registry(ref registry) = resolved_pkg.pkg_ref {
|
||||||
let latest_version = send_request(REQWEST_CLIENT.get(format!(
|
let latest_version = send_request(REQWEST_CLIENT.get(format!(
|
||||||
"{}/v0/packages/{}/{}/versions",
|
"{}/v0/packages/{}/{}/versions",
|
||||||
project.index().config()?.api(),
|
resolved_pkg.pkg_ref.get_index(project).config()?.api(),
|
||||||
registry.name.scope(),
|
registry.name.scope(),
|
||||||
registry.name.name()
|
registry.name.name()
|
||||||
)))?
|
)))?
|
||||||
|
@ -445,6 +514,7 @@ pub fn root_command(cmd: Command) -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
Command::Convert => {
|
Command::Convert => {
|
||||||
Manifest::from_path_or_convert(CWD.to_path_buf())?;
|
Manifest::from_path_or_convert(CWD.to_path_buf())?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
use std::{fs::create_dir_all, path::Path};
|
use cfg_if::cfg_if;
|
||||||
|
use std::{fs::create_dir_all, path::Path, sync::Arc};
|
||||||
|
|
||||||
use git2::{build::RepoBuilder, Repository};
|
use git2::{build::RepoBuilder, Repository};
|
||||||
use log::{debug, warn};
|
use log::{debug, error, warn};
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
index::{remote_callbacks, Index},
|
index::{remote_callbacks, CredentialsFn},
|
||||||
manifest::{Manifest, ManifestConvertError, Realm},
|
manifest::{Manifest, ManifestConvertError, Realm},
|
||||||
package_name::PackageName,
|
package_name::StandardPackageName,
|
||||||
project::Project,
|
project::{get_index, Indices},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A dependency of a package that can be downloaded from a git repository
|
/// A dependency of a package that can be downloaded from a git repository
|
||||||
|
@ -31,11 +33,11 @@ pub struct GitDependencySpecifier {
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct GitPackageRef {
|
pub struct GitPackageRef {
|
||||||
/// The name of the package
|
/// The name of the package
|
||||||
pub name: PackageName,
|
pub name: StandardPackageName,
|
||||||
/// The version of the package
|
/// The version of the package
|
||||||
pub version: Version,
|
pub version: Version,
|
||||||
/// The URL of the git repository
|
/// The URL of the git repository
|
||||||
pub repo_url: String,
|
pub repo_url: Url,
|
||||||
/// The revision of the git repository to use
|
/// The revision of the git repository to use
|
||||||
pub rev: String,
|
pub rev: String,
|
||||||
}
|
}
|
||||||
|
@ -54,13 +56,23 @@ pub enum GitDownloadError {
|
||||||
/// An error that occurred while reading the manifest of the git repository
|
/// An error that occurred while reading the manifest of the git repository
|
||||||
#[error("error reading manifest")]
|
#[error("error reading manifest")]
|
||||||
ManifestRead(#[from] ManifestConvertError),
|
ManifestRead(#[from] ManifestConvertError),
|
||||||
|
|
||||||
|
/// An error that occurred because the URL is invalid
|
||||||
|
#[error("invalid URL")]
|
||||||
|
InvalidUrl(#[from] url::ParseError),
|
||||||
|
|
||||||
|
/// An error that occurred because the manifest is not present in the git repository, and the wally feature is not enabled
|
||||||
|
#[cfg(not(feature = "wally"))]
|
||||||
|
#[error("wally feature is not enabled, but the manifest is not present in the git repository")]
|
||||||
|
ManifestNotPresent,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GitDependencySpecifier {
|
impl GitDependencySpecifier {
|
||||||
pub(crate) fn resolve<I: Index>(
|
pub(crate) fn resolve(
|
||||||
&self,
|
&self,
|
||||||
project: &Project<I>,
|
cache_dir: &Path,
|
||||||
) -> Result<(Manifest, String, String), GitDownloadError> {
|
indices: &Indices,
|
||||||
|
) -> Result<(Manifest, Url, String), GitDownloadError> {
|
||||||
debug!("resolving git dependency {}", self.repo);
|
debug!("resolving git dependency {}", self.repo);
|
||||||
|
|
||||||
// should also work with ssh urls
|
// should also work with ssh urls
|
||||||
|
@ -84,10 +96,10 @@ impl GitDependencySpecifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
let repo_url = if !is_url {
|
let repo_url = if !is_url {
|
||||||
format!("https://github.com/{}.git", &self.repo)
|
Url::parse(&format!("https://github.com/{}.git", &self.repo))
|
||||||
} else {
|
} else {
|
||||||
self.repo.to_string()
|
Url::parse(&self.repo)
|
||||||
};
|
}?;
|
||||||
|
|
||||||
if is_url {
|
if is_url {
|
||||||
debug!("assuming git repository is a url: {}", &repo_url);
|
debug!("assuming git repository is a url: {}", &repo_url);
|
||||||
|
@ -95,8 +107,7 @@ impl GitDependencySpecifier {
|
||||||
debug!("resolved git repository url to: {}", &repo_url);
|
debug!("resolved git repository url to: {}", &repo_url);
|
||||||
}
|
}
|
||||||
|
|
||||||
let dest = project
|
let dest = cache_dir
|
||||||
.cache_dir()
|
|
||||||
.join("git")
|
.join("git")
|
||||||
.join(repo_name.replace('/', "_"))
|
.join(repo_name.replace('/', "_"))
|
||||||
.join(&self.rev);
|
.join(&self.rev);
|
||||||
|
@ -105,11 +116,11 @@ impl GitDependencySpecifier {
|
||||||
create_dir_all(&dest)?;
|
create_dir_all(&dest)?;
|
||||||
|
|
||||||
let mut fetch_options = git2::FetchOptions::new();
|
let mut fetch_options = git2::FetchOptions::new();
|
||||||
fetch_options.remote_callbacks(remote_callbacks(project.index()));
|
fetch_options.remote_callbacks(remote_callbacks!(get_index(indices, None)));
|
||||||
|
|
||||||
RepoBuilder::new()
|
RepoBuilder::new()
|
||||||
.fetch_options(fetch_options)
|
.fetch_options(fetch_options)
|
||||||
.clone(&repo_url, &dest)?
|
.clone(repo_url.as_ref(), &dest)?
|
||||||
} else {
|
} else {
|
||||||
Repository::open(&dest)?
|
Repository::open(&dest)?
|
||||||
};
|
};
|
||||||
|
@ -121,7 +132,7 @@ impl GitDependencySpecifier {
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
Manifest::from_path_or_convert(dest)?,
|
Manifest::from_path_or_convert(dest)?,
|
||||||
repo_url.to_string(),
|
repo_url,
|
||||||
obj.id().to_string(),
|
obj.id().to_string(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -129,17 +140,27 @@ impl GitDependencySpecifier {
|
||||||
|
|
||||||
impl GitPackageRef {
|
impl GitPackageRef {
|
||||||
/// Downloads the package to the specified destination
|
/// Downloads the package to the specified destination
|
||||||
pub fn download<P: AsRef<Path>, I: Index>(
|
pub fn download<P: AsRef<Path>>(
|
||||||
&self,
|
&self,
|
||||||
project: &Project<I>,
|
|
||||||
dest: P,
|
dest: P,
|
||||||
|
credentials_fn: Option<Arc<CredentialsFn>>,
|
||||||
) -> Result<(), GitDownloadError> {
|
) -> Result<(), GitDownloadError> {
|
||||||
let mut fetch_options = git2::FetchOptions::new();
|
let mut fetch_options = git2::FetchOptions::new();
|
||||||
fetch_options.remote_callbacks(remote_callbacks(project.index()));
|
let mut remote_callbacks = git2::RemoteCallbacks::new();
|
||||||
|
let credentials_fn = credentials_fn.map(|f| f());
|
||||||
|
|
||||||
|
if let Some(credentials_fn) = credentials_fn {
|
||||||
|
debug!("authenticating this git clone with credentials");
|
||||||
|
remote_callbacks.credentials(credentials_fn);
|
||||||
|
} else {
|
||||||
|
debug!("no credentials provided for this git clone");
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch_options.remote_callbacks(remote_callbacks);
|
||||||
|
|
||||||
let repo = RepoBuilder::new()
|
let repo = RepoBuilder::new()
|
||||||
.fetch_options(fetch_options)
|
.fetch_options(fetch_options)
|
||||||
.clone(&self.repo_url, dest.as_ref())?;
|
.clone(self.repo_url.as_ref(), dest.as_ref())?;
|
||||||
|
|
||||||
let obj = repo.revparse_single(&self.rev)?;
|
let obj = repo.revparse_single(&self.rev)?;
|
||||||
|
|
||||||
|
@ -153,7 +174,15 @@ impl GitPackageRef {
|
||||||
|
|
||||||
repo.reset(&obj, git2::ResetType::Hard, None)?;
|
repo.reset(&obj, git2::ResetType::Hard, None)?;
|
||||||
|
|
||||||
Manifest::from_path_or_convert(dest)?;
|
cfg_if! {
|
||||||
|
if #[cfg(feature = "wally")] {
|
||||||
|
Manifest::from_path_or_convert(dest)?;
|
||||||
|
} else {
|
||||||
|
if Manifest::from_path(dest).is_err() {
|
||||||
|
return Err(GitDownloadError::ManifestNotPresent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
|
use cfg_if::cfg_if;
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use std::{fmt::Display, fs::create_dir_all, path::Path};
|
use reqwest::header::AUTHORIZATION;
|
||||||
|
use std::{fmt::Display, fs::create_dir_all, path::Path, sync::Arc};
|
||||||
|
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use serde::{de::IntoDeserializer, Deserialize, Deserializer, Serialize};
|
use serde::{de::IntoDeserializer, Deserialize, Deserializer, Serialize};
|
||||||
|
use serde_yaml::Value;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
dependencies::{
|
dependencies::{
|
||||||
|
@ -11,11 +15,11 @@ use crate::{
|
||||||
registry::{RegistryDependencySpecifier, RegistryPackageRef},
|
registry::{RegistryDependencySpecifier, RegistryPackageRef},
|
||||||
resolution::ResolvedVersionsMap,
|
resolution::ResolvedVersionsMap,
|
||||||
},
|
},
|
||||||
index::Index,
|
index::{CredentialsFn, Index},
|
||||||
manifest::Realm,
|
manifest::Realm,
|
||||||
multithread::MultithreadedJob,
|
multithread::MultithreadedJob,
|
||||||
package_name::PackageName,
|
package_name::PackageName,
|
||||||
project::{InstallProjectError, Project},
|
project::{get_index, get_index_by_url, InstallProjectError, Project},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Git dependency related stuff
|
/// Git dependency related stuff
|
||||||
|
@ -24,6 +28,9 @@ pub mod git;
|
||||||
pub mod registry;
|
pub mod registry;
|
||||||
/// Resolution
|
/// Resolution
|
||||||
pub mod resolution;
|
pub mod resolution;
|
||||||
|
/// Wally dependency related stuff
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
pub mod wally;
|
||||||
|
|
||||||
// To improve developer experience, we resolve the type of the dependency specifier with a custom deserializer, so that the user doesn't have to specify the type of the dependency
|
// To improve developer experience, we resolve the type of the dependency specifier with a custom deserializer, so that the user doesn't have to specify the type of the dependency
|
||||||
/// A dependency of a package
|
/// A dependency of a package
|
||||||
|
@ -34,6 +41,9 @@ pub enum DependencySpecifier {
|
||||||
Registry(RegistryDependencySpecifier),
|
Registry(RegistryDependencySpecifier),
|
||||||
/// A dependency that can be downloaded from a git repository
|
/// A dependency that can be downloaded from a git repository
|
||||||
Git(GitDependencySpecifier),
|
Git(GitDependencySpecifier),
|
||||||
|
/// A dependency that can be downloaded from a wally registry
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
Wally(wally::WallyDependencySpecifier),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DependencySpecifier {
|
impl DependencySpecifier {
|
||||||
|
@ -42,6 +52,8 @@ impl DependencySpecifier {
|
||||||
match self {
|
match self {
|
||||||
DependencySpecifier::Registry(registry) => registry.name.to_string(),
|
DependencySpecifier::Registry(registry) => registry.name.to_string(),
|
||||||
DependencySpecifier::Git(git) => git.repo.to_string(),
|
DependencySpecifier::Git(git) => git.repo.to_string(),
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
DependencySpecifier::Wally(wally) => wally.name.to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,6 +62,8 @@ impl DependencySpecifier {
|
||||||
match self {
|
match self {
|
||||||
DependencySpecifier::Registry(registry) => registry.version.to_string(),
|
DependencySpecifier::Registry(registry) => registry.version.to_string(),
|
||||||
DependencySpecifier::Git(git) => git.rev.clone(),
|
DependencySpecifier::Git(git) => git.rev.clone(),
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
DependencySpecifier::Wally(wally) => wally.version.to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,13 +72,15 @@ impl DependencySpecifier {
|
||||||
match self {
|
match self {
|
||||||
DependencySpecifier::Registry(registry) => registry.realm.as_ref(),
|
DependencySpecifier::Registry(registry) => registry.realm.as_ref(),
|
||||||
DependencySpecifier::Git(git) => git.realm.as_ref(),
|
DependencySpecifier::Git(git) => git.realm.as_ref(),
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
DependencySpecifier::Wally(wally) => wally.realm.as_ref(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for DependencySpecifier {
|
impl<'de> Deserialize<'de> for DependencySpecifier {
|
||||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
let yaml = serde_yaml::Value::deserialize(deserializer)?;
|
let yaml = Value::deserialize(deserializer)?;
|
||||||
|
|
||||||
let result = if yaml.get("repo").is_some() {
|
let result = if yaml.get("repo").is_some() {
|
||||||
GitDependencySpecifier::deserialize(yaml.into_deserializer())
|
GitDependencySpecifier::deserialize(yaml.into_deserializer())
|
||||||
|
@ -72,6 +88,15 @@ impl<'de> Deserialize<'de> for DependencySpecifier {
|
||||||
} else if yaml.get("name").is_some() {
|
} else if yaml.get("name").is_some() {
|
||||||
RegistryDependencySpecifier::deserialize(yaml.into_deserializer())
|
RegistryDependencySpecifier::deserialize(yaml.into_deserializer())
|
||||||
.map(DependencySpecifier::Registry)
|
.map(DependencySpecifier::Registry)
|
||||||
|
} else if yaml.get("wally").is_some() {
|
||||||
|
cfg_if! {
|
||||||
|
if #[cfg(feature = "wally")] {
|
||||||
|
wally::WallyDependencySpecifier::deserialize(yaml.into_deserializer())
|
||||||
|
.map(DependencySpecifier::Wally)
|
||||||
|
} else {
|
||||||
|
Err(serde::de::Error::custom("wally is not enabled"))
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(serde::de::Error::custom("invalid dependency"))
|
Err(serde::de::Error::custom("invalid dependency"))
|
||||||
};
|
};
|
||||||
|
@ -89,6 +114,9 @@ pub enum PackageRef {
|
||||||
Registry(RegistryPackageRef),
|
Registry(RegistryPackageRef),
|
||||||
/// A reference to a package that can be downloaded from a git repository
|
/// A reference to a package that can be downloaded from a git repository
|
||||||
Git(GitPackageRef),
|
Git(GitPackageRef),
|
||||||
|
/// A reference to a package that can be downloaded from a wally registry
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
Wally(wally::WallyPackageRef),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An error that occurred while downloading a package
|
/// An error that occurred while downloading a package
|
||||||
|
@ -101,14 +129,38 @@ pub enum DownloadError {
|
||||||
/// An error that occurred while downloading a package from a git repository
|
/// An error that occurred while downloading a package from a git repository
|
||||||
#[error("error downloading package {1} from git repository")]
|
#[error("error downloading package {1} from git repository")]
|
||||||
Git(#[source] git::GitDownloadError, Box<PackageRef>),
|
Git(#[source] git::GitDownloadError, Box<PackageRef>),
|
||||||
|
|
||||||
|
/// An error that occurred while downloading a package from a wally registry
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
#[error("error downloading package {1} from wally registry")]
|
||||||
|
Wally(#[source] wally::WallyDownloadError, Box<PackageRef>),
|
||||||
|
|
||||||
|
/// A URL is required for this type of package reference
|
||||||
|
#[error("a URL is required for this type of package reference")]
|
||||||
|
UrlRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An error that occurred while resolving a URL
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum UrlResolveError {
|
||||||
|
/// An error that occurred while resolving a URL of a registry package
|
||||||
|
#[error("error resolving URL of registry package")]
|
||||||
|
Registry(#[from] registry::RegistryUrlResolveError),
|
||||||
|
|
||||||
|
/// An error that occurred while resolving a URL of a wally package
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
#[error("error resolving URL of wally package")]
|
||||||
|
Wally(#[from] wally::ResolveWallyUrlError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PackageRef {
|
impl PackageRef {
|
||||||
/// Gets the name of the package
|
/// Gets the name of the package
|
||||||
pub fn name(&self) -> &PackageName {
|
pub fn name(&self) -> PackageName {
|
||||||
match self {
|
match self {
|
||||||
PackageRef::Registry(registry) => ®istry.name,
|
PackageRef::Registry(registry) => PackageName::Standard(registry.name.clone()),
|
||||||
PackageRef::Git(git) => &git.name,
|
PackageRef::Git(git) => PackageName::Standard(git.name.clone()),
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
PackageRef::Wally(wally) => PackageName::Wally(wally.name.clone()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,31 +169,81 @@ impl PackageRef {
|
||||||
match self {
|
match self {
|
||||||
PackageRef::Registry(registry) => ®istry.version,
|
PackageRef::Registry(registry) => ®istry.version,
|
||||||
PackageRef::Git(git) => &git.version,
|
PackageRef::Git(git) => &git.version,
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
PackageRef::Wally(wally) => &wally.version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the URL of the index
|
||||||
|
pub fn index_url(&self) -> Option<Url> {
|
||||||
|
match self {
|
||||||
|
PackageRef::Registry(registry) => Some(registry.index_url.clone()),
|
||||||
|
PackageRef::Git(_) => None,
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
PackageRef::Wally(wally) => Some(wally.index_url.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves the URL of the package
|
||||||
|
pub fn resolve_url(&self, project: &mut Project) -> Result<Option<Url>, UrlResolveError> {
|
||||||
|
Ok(match &self {
|
||||||
|
PackageRef::Registry(registry) => Some(registry.resolve_url(project.indices())?),
|
||||||
|
PackageRef::Git(_) => None,
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
PackageRef::Wally(wally) => {
|
||||||
|
let cache_dir = project.cache_dir().to_path_buf();
|
||||||
|
Some(wally.resolve_url(&cache_dir, project.indices_mut())?)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the index of the package
|
||||||
|
pub fn get_index<'a>(&self, project: &'a Project) -> &'a dyn Index {
|
||||||
|
match &self.index_url() {
|
||||||
|
Some(url) => get_index_by_url(project.indices(), url),
|
||||||
|
None => get_index(project.indices(), None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Downloads the package to the specified destination
|
/// Downloads the package to the specified destination
|
||||||
pub fn download<P: AsRef<Path>, I: Index>(
|
pub fn download<P: AsRef<Path>>(
|
||||||
&self,
|
&self,
|
||||||
project: &Project<I>,
|
reqwest_client: &reqwest::blocking::Client,
|
||||||
|
registry_auth_token: Option<String>,
|
||||||
|
url: Option<&Url>,
|
||||||
|
credentials_fn: Option<Arc<CredentialsFn>>,
|
||||||
dest: P,
|
dest: P,
|
||||||
) -> Result<(), DownloadError> {
|
) -> Result<(), DownloadError> {
|
||||||
match self {
|
match self {
|
||||||
PackageRef::Registry(registry) => registry
|
PackageRef::Registry(registry) => registry
|
||||||
.download(project, dest)
|
.download(
|
||||||
|
reqwest_client,
|
||||||
|
url.ok_or(DownloadError::UrlRequired)?,
|
||||||
|
registry_auth_token,
|
||||||
|
dest,
|
||||||
|
)
|
||||||
.map_err(|e| DownloadError::Registry(e, Box::new(self.clone()))),
|
.map_err(|e| DownloadError::Registry(e, Box::new(self.clone()))),
|
||||||
PackageRef::Git(git) => git
|
PackageRef::Git(git) => git
|
||||||
.download(project, dest)
|
.download(dest, credentials_fn)
|
||||||
.map_err(|e| DownloadError::Git(e, Box::new(self.clone()))),
|
.map_err(|e| DownloadError::Git(e, Box::new(self.clone()))),
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
PackageRef::Wally(wally) => wally
|
||||||
|
.download(
|
||||||
|
reqwest_client,
|
||||||
|
url.ok_or(DownloadError::UrlRequired)?,
|
||||||
|
registry_auth_token,
|
||||||
|
dest,
|
||||||
|
)
|
||||||
|
.map_err(|e| DownloadError::Wally(e, Box::new(self.clone()))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<I: Index> Project<I> {
|
impl Project {
|
||||||
/// Downloads the project's dependencies
|
/// Downloads the project's dependencies
|
||||||
pub fn download(
|
pub fn download(
|
||||||
&self,
|
&mut self,
|
||||||
map: &ResolvedVersionsMap,
|
map: ResolvedVersionsMap,
|
||||||
) -> Result<MultithreadedJob<DownloadError>, InstallProjectError> {
|
) -> Result<MultithreadedJob<DownloadError>, InstallProjectError> {
|
||||||
let (job, tx) = MultithreadedJob::new();
|
let (job, tx) = MultithreadedJob::new();
|
||||||
|
|
||||||
|
@ -161,10 +263,20 @@ impl<I: Index> Project<I> {
|
||||||
|
|
||||||
create_dir_all(&source)?;
|
create_dir_all(&source)?;
|
||||||
|
|
||||||
let project = self.clone();
|
let reqwest_client = self.reqwest_client.clone();
|
||||||
|
let url = resolved_package.pkg_ref.resolve_url(self)?;
|
||||||
|
let index = resolved_package.pkg_ref.get_index(self);
|
||||||
|
let registry_auth_token = index.registry_auth_token().map(|t| t.to_string());
|
||||||
|
let credentials_fn = index.credentials_fn().cloned();
|
||||||
|
|
||||||
job.execute(&tx, move || {
|
job.execute(&tx, move || {
|
||||||
resolved_package.pkg_ref.download(&project, source)
|
resolved_package.pkg_ref.download(
|
||||||
|
&reqwest_client,
|
||||||
|
registry_auth_token,
|
||||||
|
url.as_ref(),
|
||||||
|
credentials_fn,
|
||||||
|
source,
|
||||||
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -178,3 +290,24 @@ impl Display for PackageRef {
|
||||||
write!(f, "{}@{}", self.name(), self.version())
|
write!(f, "{}@{}", self.name(), self.version())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn maybe_authenticated_request(
|
||||||
|
reqwest_client: &reqwest::blocking::Client,
|
||||||
|
url: &str,
|
||||||
|
registry_auth_token: Option<String>,
|
||||||
|
) -> reqwest::blocking::RequestBuilder {
|
||||||
|
let mut builder = reqwest_client.get(url);
|
||||||
|
debug!("sending request to {}", url);
|
||||||
|
|
||||||
|
if let Some(token) = registry_auth_token {
|
||||||
|
let hidden_token = token
|
||||||
|
.chars()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, c)| if i <= 8 { c } else { '*' })
|
||||||
|
.collect::<String>();
|
||||||
|
debug!("with registry token {hidden_token}");
|
||||||
|
builder = builder.header(AUTHORIZATION, format!("Bearer {token}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
builder
|
||||||
|
}
|
||||||
|
|
|
@ -1,26 +1,33 @@
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use log::{debug, error};
|
use log::{debug, error};
|
||||||
use reqwest::header::{AUTHORIZATION, USER_AGENT as USER_AGENT_HEADER};
|
|
||||||
use semver::{Version, VersionReq};
|
use semver::{Version, VersionReq};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
index::Index, manifest::Realm, package_name::PackageName, project::Project, USER_AGENT,
|
dependencies::maybe_authenticated_request,
|
||||||
|
manifest::Realm,
|
||||||
|
package_name::StandardPackageName,
|
||||||
|
project::{get_index_by_url, Indices, DEFAULT_INDEX_NAME},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn default_index_name() -> String {
|
||||||
|
DEFAULT_INDEX_NAME.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
/// A dependency of a package that can be downloaded from a registry
|
/// A dependency of a package that can be downloaded from a registry
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct RegistryDependencySpecifier {
|
pub struct RegistryDependencySpecifier {
|
||||||
/// The name of the package
|
/// The name of the package
|
||||||
pub name: PackageName,
|
pub name: StandardPackageName,
|
||||||
/// The version requirement of the package
|
/// The version requirement of the package
|
||||||
pub version: VersionReq,
|
pub version: VersionReq,
|
||||||
// TODO: support per-package registries
|
/// The name of the index to use
|
||||||
// #[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(default = "default_index_name")]
|
||||||
// pub registry: Option<String>,
|
pub index: String,
|
||||||
/// The realm of the package
|
/// The realm of the package
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub realm: Option<Realm>,
|
pub realm: Option<Realm>,
|
||||||
|
@ -31,12 +38,11 @@ pub struct RegistryDependencySpecifier {
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct RegistryPackageRef {
|
pub struct RegistryPackageRef {
|
||||||
/// The name of the package
|
/// The name of the package
|
||||||
pub name: PackageName,
|
pub name: StandardPackageName,
|
||||||
/// The version of the package
|
/// The version of the package
|
||||||
pub version: Version,
|
pub version: Version,
|
||||||
// TODO: support per-package registries
|
/// The index URL of the package
|
||||||
// #[serde(skip_serializing_if = "Option::is_none")]
|
pub index_url: Url,
|
||||||
// pub index_url: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An error that occurred while downloading a package from a registry
|
/// An error that occurred while downloading a package from a registry
|
||||||
|
@ -56,48 +62,64 @@ pub enum RegistryDownloadError {
|
||||||
|
|
||||||
/// The package was not found on the registry
|
/// The package was not found on the registry
|
||||||
#[error("package {0} not found on the registry, but found in the index")]
|
#[error("package {0} not found on the registry, but found in the index")]
|
||||||
NotFound(PackageName),
|
NotFound(StandardPackageName),
|
||||||
|
|
||||||
/// The user is unauthorized to download the package
|
/// The user is unauthorized to download the package
|
||||||
#[error("unauthorized to download package {0}")]
|
#[error("unauthorized to download package {0}")]
|
||||||
Unauthorized(PackageName),
|
Unauthorized(StandardPackageName),
|
||||||
|
|
||||||
/// An HTTP error occurred
|
/// An HTTP error occurred
|
||||||
#[error("http error {0}: the server responded with {1}")]
|
#[error("http error {0}: the server responded with {1}")]
|
||||||
Http(reqwest::StatusCode, String),
|
Http(reqwest::StatusCode, String),
|
||||||
|
|
||||||
|
/// An error occurred while parsing the api URL
|
||||||
|
#[error("error parsing the API URL")]
|
||||||
|
UrlParse(#[from] url::ParseError),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An error that occurred while resolving the url of a registry package
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum RegistryUrlResolveError {
|
||||||
|
/// An error that occurred while reading the index config
|
||||||
|
#[error("error with the index config")]
|
||||||
|
IndexConfig(#[from] crate::index::ConfigError),
|
||||||
|
|
||||||
|
/// An error occurred while parsing the api URL
|
||||||
|
#[error("error parsing the API URL")]
|
||||||
|
UrlParse(#[from] url::ParseError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RegistryPackageRef {
|
impl RegistryPackageRef {
|
||||||
/// Downloads the package to the specified destination
|
/// Resolves the download URL of the package
|
||||||
pub fn download<P: AsRef<Path>, I: Index>(
|
pub fn resolve_url(&self, indices: &Indices) -> Result<Url, RegistryUrlResolveError> {
|
||||||
&self,
|
let index = get_index_by_url(indices, &self.index_url);
|
||||||
project: &Project<I>,
|
let config = index.config()?;
|
||||||
dest: P,
|
|
||||||
) -> Result<(), RegistryDownloadError> {
|
let url = config
|
||||||
let url = project
|
|
||||||
.index()
|
|
||||||
.config()?
|
|
||||||
.download()
|
.download()
|
||||||
.replace("{PACKAGE_AUTHOR}", self.name.scope())
|
.replace("{PACKAGE_AUTHOR}", self.name.scope())
|
||||||
.replace("{PACKAGE_NAME}", self.name.name())
|
.replace("{PACKAGE_NAME}", self.name.name())
|
||||||
.replace("{PACKAGE_VERSION}", &self.version.to_string());
|
.replace("{PACKAGE_VERSION}", &self.version.to_string());
|
||||||
|
|
||||||
|
Ok(Url::parse(&url)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Downloads the package to the specified destination
|
||||||
|
pub fn download<P: AsRef<Path>>(
|
||||||
|
&self,
|
||||||
|
reqwest_client: &reqwest::blocking::Client,
|
||||||
|
url: &Url,
|
||||||
|
registry_auth_token: Option<String>,
|
||||||
|
dest: P,
|
||||||
|
) -> Result<(), RegistryDownloadError> {
|
||||||
debug!(
|
debug!(
|
||||||
"downloading registry package {}@{} from {}",
|
"downloading registry package {}@{} from {}",
|
||||||
self.name, self.version, url
|
self.name, self.version, url
|
||||||
);
|
);
|
||||||
|
|
||||||
let client = reqwest::blocking::Client::new();
|
let response =
|
||||||
let response = {
|
maybe_authenticated_request(reqwest_client, url.as_str(), registry_auth_token)
|
||||||
let mut builder = client.get(&url).header(USER_AGENT_HEADER, USER_AGENT);
|
.send()?;
|
||||||
if let Some(token) = project.registry_auth_token() {
|
|
||||||
let visible_tokens = token.chars().take(8).collect::<String>();
|
|
||||||
let hidden_tokens = "*".repeat(token.len() - 8);
|
|
||||||
debug!("using registry token {visible_tokens}{hidden_tokens}");
|
|
||||||
builder = builder.header(AUTHORIZATION, format!("Bearer {}", token));
|
|
||||||
}
|
|
||||||
builder.send()?
|
|
||||||
};
|
|
||||||
|
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
return match response.status() {
|
return match response.status() {
|
||||||
|
|
|
@ -9,16 +9,18 @@ use semver::Version;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
use crate::index::Index;
|
||||||
use crate::{
|
use crate::{
|
||||||
dependencies::{
|
dependencies::{
|
||||||
git::{GitDownloadError, GitPackageRef},
|
git::{GitDownloadError, GitPackageRef},
|
||||||
registry::{RegistryDependencySpecifier, RegistryPackageRef},
|
registry::RegistryPackageRef,
|
||||||
DependencySpecifier, PackageRef,
|
DependencySpecifier, PackageRef,
|
||||||
},
|
},
|
||||||
index::{Index, IndexPackageError},
|
index::IndexPackageError,
|
||||||
manifest::{DependencyType, Manifest, Realm},
|
manifest::{DependencyType, Manifest, Realm},
|
||||||
package_name::PackageName,
|
package_name::PackageName,
|
||||||
project::{Project, ReadLockfileError},
|
project::{get_index, get_index_by_url, Project, ReadLockfileError},
|
||||||
DEV_PACKAGES_FOLDER, INDEX_FOLDER, PACKAGES_FOLDER, SERVER_PACKAGES_FOLDER,
|
DEV_PACKAGES_FOLDER, INDEX_FOLDER, PACKAGES_FOLDER, SERVER_PACKAGES_FOLDER,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -135,15 +137,15 @@ pub enum ResolveError {
|
||||||
|
|
||||||
/// An error that occurred because a registry dependency conflicts with a git dependency
|
/// An error that occurred because a registry dependency conflicts with a git dependency
|
||||||
#[error("registry dependency {0}@{1} conflicts with git dependency")]
|
#[error("registry dependency {0}@{1} conflicts with git dependency")]
|
||||||
RegistryConflict(PackageName, Version),
|
RegistryConflict(String, Version),
|
||||||
|
|
||||||
/// An error that occurred because a git dependency conflicts with a registry dependency
|
/// An error that occurred because a git dependency conflicts with a registry dependency
|
||||||
#[error("git dependency {0}@{1} conflicts with registry dependency")]
|
#[error("git dependency {0}@{1} conflicts with registry dependency")]
|
||||||
GitConflict(PackageName, Version),
|
GitConflict(String, Version),
|
||||||
|
|
||||||
/// An error that occurred because no satisfying version was found for a dependency
|
/// An error that occurred because no satisfying version was found for a dependency
|
||||||
#[error("no satisfying version found for dependency {0:?}")]
|
#[error("no satisfying version found for dependency {0:?}")]
|
||||||
NoSatisfyingVersion(RegistryDependencySpecifier),
|
NoSatisfyingVersion(Box<DependencySpecifier>),
|
||||||
|
|
||||||
/// An error that occurred while downloading a package from a git repository
|
/// An error that occurred while downloading a package from a git repository
|
||||||
#[error("error downloading git package")]
|
#[error("error downloading git package")]
|
||||||
|
@ -151,11 +153,11 @@ pub enum ResolveError {
|
||||||
|
|
||||||
/// An error that occurred because a package was not found in the index
|
/// An error that occurred because a package was not found in the index
|
||||||
#[error("package {0} not found in index")]
|
#[error("package {0} not found in index")]
|
||||||
PackageNotFound(PackageName),
|
PackageNotFound(String),
|
||||||
|
|
||||||
/// An error that occurred while getting a package from the index
|
/// An error that occurred while getting a package from the index
|
||||||
#[error("failed to get package {1} from index")]
|
#[error("failed to get package {1} from index")]
|
||||||
IndexPackage(#[source] IndexPackageError, PackageName),
|
IndexPackage(#[source] IndexPackageError, String),
|
||||||
|
|
||||||
/// An error that occurred while reading the lockfile
|
/// An error that occurred while reading the lockfile
|
||||||
#[error("failed to read lockfile")]
|
#[error("failed to read lockfile")]
|
||||||
|
@ -167,18 +169,27 @@ pub enum ResolveError {
|
||||||
|
|
||||||
/// An error that occurred because two realms are incompatible
|
/// An error that occurred because two realms are incompatible
|
||||||
#[error("incompatible realms for package {0} (package specified {1}, user specified {2})")]
|
#[error("incompatible realms for package {0} (package specified {1}, user specified {2})")]
|
||||||
IncompatibleRealms(PackageName, Realm, Realm),
|
IncompatibleRealms(String, Realm, Realm),
|
||||||
|
|
||||||
/// An error that occurred because a peer dependency is not installed
|
/// An error that occurred because a peer dependency is not installed
|
||||||
#[error("peer dependency {0}@{1} is not installed")]
|
#[error("peer dependency {0}@{1} is not installed")]
|
||||||
PeerNotInstalled(PackageName, Version),
|
PeerNotInstalled(String, Version),
|
||||||
|
|
||||||
|
/// An error that occurred while cloning a wally index
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
#[error("error cloning wally index")]
|
||||||
|
CloneWallyIndex(#[from] crate::dependencies::wally::CloneWallyIndexError),
|
||||||
|
|
||||||
|
/// An error that occurred while parsing a URL
|
||||||
|
#[error("error parsing URL")]
|
||||||
|
UrlParse(#[from] url::ParseError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Manifest {
|
impl Manifest {
|
||||||
/// Resolves the dependency tree for the project
|
/// Resolves the dependency tree for the project
|
||||||
pub fn dependency_tree<I: Index>(
|
pub fn dependency_tree(
|
||||||
&self,
|
&self,
|
||||||
project: &Project<I>,
|
project: &mut Project,
|
||||||
locked: bool,
|
locked: bool,
|
||||||
) -> Result<ResolvedVersionsMap, ResolveError> {
|
) -> Result<ResolvedVersionsMap, ResolveError> {
|
||||||
debug!("resolving dependency tree for project {}", self.name);
|
debug!("resolving dependency tree for project {}", self.name);
|
||||||
|
@ -253,19 +264,23 @@ impl Manifest {
|
||||||
while let Some(((specifier, dep_type), dependant)) = queue.pop_front() {
|
while let Some(((specifier, dep_type), dependant)) = queue.pop_front() {
|
||||||
let (pkg_ref, default_realm, dependencies) = match &specifier {
|
let (pkg_ref, default_realm, dependencies) = match &specifier {
|
||||||
DependencySpecifier::Registry(registry_dependency) => {
|
DependencySpecifier::Registry(registry_dependency) => {
|
||||||
let index_entries = project
|
let index = if dependant.is_none() {
|
||||||
.index()
|
get_index(project.indices(), Some(®istry_dependency.index))
|
||||||
.package(®istry_dependency.name)
|
} else {
|
||||||
|
get_index_by_url(project.indices(), ®istry_dependency.index.parse()?)
|
||||||
|
};
|
||||||
|
let pkg_name: PackageName = registry_dependency.name.clone().into();
|
||||||
|
|
||||||
|
let index_entries = index
|
||||||
|
.package(&pkg_name)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
ResolveError::IndexPackage(e, registry_dependency.name.clone())
|
ResolveError::IndexPackage(e, registry_dependency.name.to_string())
|
||||||
})?
|
})?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
ResolveError::PackageNotFound(registry_dependency.name.clone())
|
ResolveError::PackageNotFound(registry_dependency.name.to_string())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let resolved_versions = resolved_versions_map
|
let resolved_versions = resolved_versions_map.entry(pkg_name).or_default();
|
||||||
.entry(registry_dependency.name.clone())
|
|
||||||
.or_default();
|
|
||||||
|
|
||||||
// try to find the highest already downloaded version that satisfies the requirement, otherwise find the highest satisfying version in the index
|
// try to find the highest already downloaded version that satisfies the requirement, otherwise find the highest satisfying version in the index
|
||||||
let Some(version) =
|
let Some(version) =
|
||||||
|
@ -278,9 +293,9 @@ impl Manifest {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
else {
|
else {
|
||||||
return Err(ResolveError::NoSatisfyingVersion(
|
return Err(ResolveError::NoSatisfyingVersion(Box::new(
|
||||||
registry_dependency.clone(),
|
specifier.clone(),
|
||||||
));
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
let entry = index_entries
|
let entry = index_entries
|
||||||
|
@ -297,13 +312,15 @@ impl Manifest {
|
||||||
PackageRef::Registry(RegistryPackageRef {
|
PackageRef::Registry(RegistryPackageRef {
|
||||||
name: registry_dependency.name.clone(),
|
name: registry_dependency.name.clone(),
|
||||||
version: version.clone(),
|
version: version.clone(),
|
||||||
|
index_url: index.url().clone(),
|
||||||
}),
|
}),
|
||||||
entry.realm,
|
entry.realm,
|
||||||
entry.dependencies,
|
entry.dependencies,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
DependencySpecifier::Git(git_dependency) => {
|
DependencySpecifier::Git(git_dependency) => {
|
||||||
let (manifest, url, rev) = git_dependency.resolve(project)?;
|
let (manifest, url, rev) =
|
||||||
|
git_dependency.resolve(project.cache_dir(), project.indices())?;
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"resolved git dependency {} to {url}#{rev}",
|
"resolved git dependency {} to {url}#{rev}",
|
||||||
|
@ -321,6 +338,61 @@ impl Manifest {
|
||||||
manifest.dependencies(),
|
manifest.dependencies(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
DependencySpecifier::Wally(wally_dependency) => {
|
||||||
|
let cache_dir = project.cache_dir().to_path_buf();
|
||||||
|
let index = crate::dependencies::wally::clone_wally_index(
|
||||||
|
&cache_dir,
|
||||||
|
project.indices_mut(),
|
||||||
|
&wally_dependency.index_url,
|
||||||
|
)?;
|
||||||
|
let pkg_name = wally_dependency.name.clone().into();
|
||||||
|
|
||||||
|
let index_entries = index
|
||||||
|
.package(&pkg_name)
|
||||||
|
.map_err(|e| {
|
||||||
|
ResolveError::IndexPackage(e, wally_dependency.name.to_string())
|
||||||
|
})?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ResolveError::PackageNotFound(wally_dependency.name.to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let resolved_versions = resolved_versions_map.entry(pkg_name).or_default();
|
||||||
|
|
||||||
|
// try to find the highest already downloaded version that satisfies the requirement, otherwise find the highest satisfying version in the index
|
||||||
|
let Some(version) = find_highest!(resolved_versions.keys(), wally_dependency)
|
||||||
|
.or_else(|| {
|
||||||
|
find_highest!(
|
||||||
|
index_entries.iter().map(|v| &v.version),
|
||||||
|
wally_dependency
|
||||||
|
)
|
||||||
|
})
|
||||||
|
else {
|
||||||
|
return Err(ResolveError::NoSatisfyingVersion(Box::new(
|
||||||
|
specifier.clone(),
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
|
||||||
|
let entry = index_entries
|
||||||
|
.into_iter()
|
||||||
|
.find(|e| e.version.eq(&version))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"resolved registry dependency {} to {}",
|
||||||
|
wally_dependency.name, version
|
||||||
|
);
|
||||||
|
|
||||||
|
(
|
||||||
|
PackageRef::Wally(crate::dependencies::wally::WallyPackageRef {
|
||||||
|
name: wally_dependency.name.clone(),
|
||||||
|
version: version.clone(),
|
||||||
|
index_url: index.url().clone(),
|
||||||
|
}),
|
||||||
|
entry.realm,
|
||||||
|
entry.dependencies,
|
||||||
|
)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let is_root = dependant.is_none();
|
let is_root = dependant.is_none();
|
||||||
|
@ -337,7 +409,7 @@ impl Manifest {
|
||||||
.and_then(|v| v.get_mut(&dependant_version))
|
.and_then(|v| v.get_mut(&dependant_version))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.dependencies
|
.dependencies
|
||||||
.insert((pkg_ref.name().clone(), pkg_ref.version().clone()));
|
.insert((pkg_ref.name(), pkg_ref.version().clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let resolved_versions = resolved_versions_map
|
let resolved_versions = resolved_versions_map
|
||||||
|
@ -348,12 +420,15 @@ impl Manifest {
|
||||||
match (&pkg_ref, &previously_resolved.pkg_ref) {
|
match (&pkg_ref, &previously_resolved.pkg_ref) {
|
||||||
(PackageRef::Registry(r), PackageRef::Git(_g)) => {
|
(PackageRef::Registry(r), PackageRef::Git(_g)) => {
|
||||||
return Err(ResolveError::RegistryConflict(
|
return Err(ResolveError::RegistryConflict(
|
||||||
r.name.clone(),
|
r.name.to_string(),
|
||||||
r.version.clone(),
|
r.version.clone(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
(PackageRef::Git(g), PackageRef::Registry(_r)) => {
|
(PackageRef::Git(g), PackageRef::Registry(_r)) => {
|
||||||
return Err(ResolveError::GitConflict(g.name.clone(), g.version.clone()));
|
return Err(ResolveError::GitConflict(
|
||||||
|
g.name.to_string(),
|
||||||
|
g.version.clone(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
@ -374,7 +449,7 @@ impl Manifest {
|
||||||
&& default_realm.is_some_and(|realm| realm == Realm::Server)
|
&& default_realm.is_some_and(|realm| realm == Realm::Server)
|
||||||
{
|
{
|
||||||
return Err(ResolveError::IncompatibleRealms(
|
return Err(ResolveError::IncompatibleRealms(
|
||||||
pkg_ref.name().clone(),
|
pkg_ref.name().to_string(),
|
||||||
default_realm.unwrap(),
|
default_realm.unwrap(),
|
||||||
*specifier.realm().unwrap(),
|
*specifier.realm().unwrap(),
|
||||||
));
|
));
|
||||||
|
@ -410,7 +485,7 @@ impl Manifest {
|
||||||
for (version, resolved_package) in versions {
|
for (version, resolved_package) in versions {
|
||||||
if resolved_package.dep_type == DependencyType::Peer {
|
if resolved_package.dep_type == DependencyType::Peer {
|
||||||
return Err(ResolveError::PeerNotInstalled(
|
return Err(ResolveError::PeerNotInstalled(
|
||||||
resolved_package.pkg_ref.name().clone(),
|
resolved_package.pkg_ref.name().to_string(),
|
||||||
resolved_package.pkg_ref.version().clone(),
|
resolved_package.pkg_ref.version().clone(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
364
src/dependencies/wally.rs
Normal file
364
src/dependencies/wally.rs
Normal file
|
@ -0,0 +1,364 @@
|
||||||
|
use std::{
|
||||||
|
collections::BTreeMap,
|
||||||
|
fs::{create_dir_all, read},
|
||||||
|
hash::{DefaultHasher, Hash, Hasher},
|
||||||
|
io::Cursor,
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
|
||||||
|
use git2::build::RepoBuilder;
|
||||||
|
use log::{debug, error};
|
||||||
|
use semver::{Version, VersionReq};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
dependencies::{maybe_authenticated_request, DependencySpecifier},
|
||||||
|
index::{remote_callbacks, IndexFileEntry, WallyIndex},
|
||||||
|
manifest::{DependencyType, Manifest, ManifestConvertError, Realm},
|
||||||
|
package_name::{
|
||||||
|
FromStrPackageNameParseError, WallyPackageName, WallyPackageNameValidationError,
|
||||||
|
},
|
||||||
|
project::{get_wally_index, Indices},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A dependency of a package that can be downloaded from a registry
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct WallyDependencySpecifier {
|
||||||
|
/// The name of the package
|
||||||
|
#[serde(rename = "wally")]
|
||||||
|
pub name: WallyPackageName,
|
||||||
|
/// The version requirement of the package
|
||||||
|
pub version: VersionReq,
|
||||||
|
/// The url of the index
|
||||||
|
pub index_url: Url,
|
||||||
|
/// The realm of the package
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub realm: Option<Realm>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A reference to a package that can be downloaded from a registry
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct WallyPackageRef {
|
||||||
|
/// The name of the package
|
||||||
|
pub name: WallyPackageName,
|
||||||
|
/// The version of the package
|
||||||
|
pub version: Version,
|
||||||
|
/// The index URL of the package
|
||||||
|
pub index_url: Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An error that occurred while downloading a package from a wally registry
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum WallyDownloadError {
|
||||||
|
/// An error that occurred while interacting with reqwest
|
||||||
|
#[error("error interacting with reqwest")]
|
||||||
|
Reqwest(#[from] reqwest::Error),
|
||||||
|
|
||||||
|
/// An error that occurred while interacting with the file system
|
||||||
|
#[error("error interacting with the file system")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
|
/// The package was not found on the registry
|
||||||
|
#[error("package {0} not found on the registry, but found in the index")]
|
||||||
|
NotFound(WallyPackageName),
|
||||||
|
|
||||||
|
/// The user is unauthorized to download the package
|
||||||
|
#[error("unauthorized to download package {0}")]
|
||||||
|
Unauthorized(WallyPackageName),
|
||||||
|
|
||||||
|
/// An HTTP error occurred
|
||||||
|
#[error("http error {0}: the server responded with {1}")]
|
||||||
|
Http(reqwest::StatusCode, String),
|
||||||
|
|
||||||
|
/// An error occurred while extracting the archive
|
||||||
|
#[error("error extracting archive")]
|
||||||
|
Zip(#[from] zip::result::ZipError),
|
||||||
|
|
||||||
|
/// An error occurred while interacting with git
|
||||||
|
#[error("error interacting with git")]
|
||||||
|
Git(#[from] git2::Error),
|
||||||
|
|
||||||
|
/// An error occurred while interacting with serde
|
||||||
|
#[error("error interacting with serde")]
|
||||||
|
Serde(#[from] serde_json::Error),
|
||||||
|
|
||||||
|
/// An error occurred while parsing the api URL
|
||||||
|
#[error("error parsing URL")]
|
||||||
|
Url(#[from] url::ParseError),
|
||||||
|
|
||||||
|
/// An error occurred while refreshing the index
|
||||||
|
#[error("error refreshing index")]
|
||||||
|
RefreshIndex(#[from] crate::index::RefreshError),
|
||||||
|
|
||||||
|
/// An error occurred while converting the manifest
|
||||||
|
#[error("error converting manifest")]
|
||||||
|
Manifest(#[from] ManifestConvertError),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An error that occurred while cloning a wally index
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum CloneWallyIndexError {
|
||||||
|
/// An error that occurred while interacting with git
|
||||||
|
#[error("error interacting with git")]
|
||||||
|
Git(#[from] git2::Error),
|
||||||
|
|
||||||
|
/// An error that occurred while interacting with the file system
|
||||||
|
#[error("error interacting with the file system")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
|
/// An error that occurred while refreshing the index
|
||||||
|
#[error("error refreshing index")]
|
||||||
|
RefreshIndex(#[from] crate::index::RefreshError),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn clone_wally_index(
|
||||||
|
cache_dir: &Path,
|
||||||
|
indices: &mut Indices,
|
||||||
|
index_url: &Url,
|
||||||
|
) -> Result<WallyIndex, CloneWallyIndexError> {
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
index_url.hash(&mut hasher);
|
||||||
|
let url_hash = hasher.finish().to_string();
|
||||||
|
|
||||||
|
let index_path = cache_dir.join("wally_indices").join(url_hash);
|
||||||
|
|
||||||
|
if index_path.exists() {
|
||||||
|
debug!("wally index already exists at {}", index_path.display());
|
||||||
|
|
||||||
|
return Ok(get_wally_index(indices, index_url, Some(&index_path))?.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"cloning wally index from {} to {}",
|
||||||
|
index_url,
|
||||||
|
index_path.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
create_dir_all(&index_path)?;
|
||||||
|
|
||||||
|
let mut fetch_options = git2::FetchOptions::new();
|
||||||
|
fetch_options.remote_callbacks(remote_callbacks!(get_wally_index(
|
||||||
|
indices,
|
||||||
|
index_url,
|
||||||
|
Some(&index_path)
|
||||||
|
)?));
|
||||||
|
|
||||||
|
RepoBuilder::new()
|
||||||
|
.fetch_options(fetch_options)
|
||||||
|
.clone(index_url.as_ref(), &index_path)?;
|
||||||
|
|
||||||
|
Ok(get_wally_index(indices, index_url, Some(&index_path))?.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The configuration of a wally index
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
struct WallyIndexConfig {
|
||||||
|
/// The URL of the wally API
|
||||||
|
api: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An error that occurred while resolving the URL of a wally package
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum ResolveWallyUrlError {
|
||||||
|
/// An error that occurred while interacting with the file system
|
||||||
|
#[error("error interacting with the file system")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
|
/// An error that occurred while interacting with the index
|
||||||
|
#[error("error interacting with the index")]
|
||||||
|
Index(#[from] crate::index::ConfigError),
|
||||||
|
|
||||||
|
/// An error that occurred while parsing the URL
|
||||||
|
#[error("error parsing URL")]
|
||||||
|
Url(#[from] url::ParseError),
|
||||||
|
|
||||||
|
/// An error that occurred while cloning the index
|
||||||
|
#[error("error cloning index")]
|
||||||
|
CloneIndex(#[from] CloneWallyIndexError),
|
||||||
|
|
||||||
|
/// An error that occurred while reading the index config
|
||||||
|
#[error("error reading index config")]
|
||||||
|
ReadConfig(#[from] serde_json::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_api_url(index_path: &Path) -> Result<String, ResolveWallyUrlError> {
|
||||||
|
let config_path = index_path.join("config.json");
|
||||||
|
let raw_config_contents = read(config_path)?;
|
||||||
|
let config: WallyIndexConfig = serde_json::from_slice(&raw_config_contents)?;
|
||||||
|
|
||||||
|
Ok(config.api)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WallyPackageRef {
|
||||||
|
/// Resolves the download URL of the package
|
||||||
|
pub fn resolve_url(
|
||||||
|
&self,
|
||||||
|
cache_dir: &Path,
|
||||||
|
indices: &mut Indices,
|
||||||
|
) -> Result<Url, ResolveWallyUrlError> {
|
||||||
|
let index = clone_wally_index(cache_dir, indices, &self.index_url)?;
|
||||||
|
|
||||||
|
let api_url = Url::parse(&read_api_url(&index.path)?)?;
|
||||||
|
|
||||||
|
let url = format!(
|
||||||
|
"{}/v1/package-contents/{}/{}/{}",
|
||||||
|
api_url.to_string().trim_end_matches('/'),
|
||||||
|
self.name.scope(),
|
||||||
|
self.name.name(),
|
||||||
|
self.version
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Url::parse(&url)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Downloads the package to the specified destination
|
||||||
|
pub fn download<P: AsRef<Path>>(
|
||||||
|
&self,
|
||||||
|
reqwest_client: &reqwest::blocking::Client,
|
||||||
|
url: &Url,
|
||||||
|
registry_auth_token: Option<String>,
|
||||||
|
dest: P,
|
||||||
|
) -> Result<(), WallyDownloadError> {
|
||||||
|
let response =
|
||||||
|
maybe_authenticated_request(reqwest_client, url.as_str(), registry_auth_token)
|
||||||
|
.header("Wally-Version", "0.3.2")
|
||||||
|
.send()?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return match response.status() {
|
||||||
|
reqwest::StatusCode::NOT_FOUND => {
|
||||||
|
Err(WallyDownloadError::NotFound(self.name.clone()))
|
||||||
|
}
|
||||||
|
reqwest::StatusCode::UNAUTHORIZED => {
|
||||||
|
Err(WallyDownloadError::Unauthorized(self.name.clone()))
|
||||||
|
}
|
||||||
|
_ => Err(WallyDownloadError::Http(
|
||||||
|
response.status(),
|
||||||
|
response.text()?,
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = response.bytes()?;
|
||||||
|
|
||||||
|
let mut archive = zip::read::ZipArchive::new(Cursor::new(bytes))?;
|
||||||
|
archive.extract(dest.as_ref())?;
|
||||||
|
|
||||||
|
Manifest::from_path_or_convert(dest.as_ref())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub(crate) struct WallyPackage {
|
||||||
|
pub(crate) name: String,
|
||||||
|
pub(crate) version: Version,
|
||||||
|
pub(crate) registry: Url,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) realm: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) description: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) license: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) authors: Option<Vec<String>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) private: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Default, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub(crate) struct WallyPlace {
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) shared_packages: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) server_packages: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub(crate) struct WallyManifest {
|
||||||
|
pub(crate) package: WallyPackage,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) place: WallyPlace,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) dependencies: BTreeMap<String, String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) server_dependencies: BTreeMap<String, String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) dev_dependencies: BTreeMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An error that occurred while converting a wally manifest's dependencies
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum WallyManifestDependencyError {
|
||||||
|
/// An error that occurred because the dependency specifier is invalid
|
||||||
|
#[error("invalid dependency specifier: {0}")]
|
||||||
|
InvalidDependencySpecifier(String),
|
||||||
|
|
||||||
|
/// An error that occurred while parsing a package name
|
||||||
|
#[error("error parsing package name")]
|
||||||
|
PackageName(#[from] FromStrPackageNameParseError<WallyPackageNameValidationError>),
|
||||||
|
|
||||||
|
/// An error that occurred while parsing a version requirement
|
||||||
|
#[error("error parsing version requirement")]
|
||||||
|
VersionReq(#[from] semver::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parse_wally_dependencies(
|
||||||
|
manifest: WallyManifest,
|
||||||
|
) -> Result<Vec<DependencySpecifier>, WallyManifestDependencyError> {
|
||||||
|
[
|
||||||
|
(manifest.dependencies, Realm::Shared),
|
||||||
|
(manifest.server_dependencies, Realm::Server),
|
||||||
|
(manifest.dev_dependencies, Realm::Development),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|(deps, realm)| {
|
||||||
|
deps.into_values()
|
||||||
|
.map(|specifier| {
|
||||||
|
let (name, req) = specifier.split_once('@').ok_or_else(|| {
|
||||||
|
WallyManifestDependencyError::InvalidDependencySpecifier(specifier.clone())
|
||||||
|
})?;
|
||||||
|
let name: WallyPackageName = name.parse()?;
|
||||||
|
let req: VersionReq = req.parse()?;
|
||||||
|
|
||||||
|
Ok(DependencySpecifier::Wally(WallyDependencySpecifier {
|
||||||
|
name,
|
||||||
|
version: req,
|
||||||
|
index_url: manifest.package.registry.clone(),
|
||||||
|
realm: Some(realm),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<WallyManifest> for IndexFileEntry {
|
||||||
|
type Error = WallyManifestDependencyError;
|
||||||
|
|
||||||
|
fn try_from(value: WallyManifest) -> Result<Self, Self::Error> {
|
||||||
|
let dependencies = parse_wally_dependencies(value.clone())?
|
||||||
|
.into_iter()
|
||||||
|
.map(|d| (d, DependencyType::Normal))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(IndexFileEntry {
|
||||||
|
version: value.package.version,
|
||||||
|
realm: value
|
||||||
|
.package
|
||||||
|
.realm
|
||||||
|
.map(|r| r.parse().unwrap_or(Realm::Shared)),
|
||||||
|
published_at: Default::default(),
|
||||||
|
description: value.package.description,
|
||||||
|
dependencies,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
349
src/index.rs
349
src/index.rs
|
@ -1,5 +1,5 @@
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use std::{
|
use std::{
|
||||||
|
any::Any,
|
||||||
collections::BTreeSet,
|
collections::BTreeSet,
|
||||||
fmt::Debug,
|
fmt::Debug,
|
||||||
fs::create_dir_all,
|
fs::create_dir_all,
|
||||||
|
@ -8,11 +8,13 @@ use std::{
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use git2::{build::RepoBuilder, Remote, Repository, Signature};
|
use git2::{build::RepoBuilder, Remote, Repository, Signature};
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
dependencies::DependencySpecifier,
|
dependencies::DependencySpecifier,
|
||||||
|
@ -24,7 +26,7 @@ use crate::{
|
||||||
pub type ScopeOwners = BTreeSet<u64>;
|
pub type ScopeOwners = BTreeSet<u64>;
|
||||||
|
|
||||||
/// A packages index
|
/// A packages index
|
||||||
pub trait Index: Send + Sync + Debug + Clone + 'static {
|
pub trait Index: Send + Sync + Debug + Any + 'static {
|
||||||
/// Gets the owners of a scope
|
/// Gets the owners of a scope
|
||||||
fn scope_owners(&self, scope: &str) -> Result<Option<ScopeOwners>, ScopeOwnersError>;
|
fn scope_owners(&self, scope: &str) -> Result<Option<ScopeOwners>, ScopeOwnersError>;
|
||||||
|
|
||||||
|
@ -50,6 +52,22 @@ pub trait Index: Send + Sync + Debug + Clone + 'static {
|
||||||
|
|
||||||
/// Returns a function that gets the credentials for a git repository
|
/// Returns a function that gets the credentials for a git repository
|
||||||
fn credentials_fn(&self) -> Option<&Arc<CredentialsFn>>;
|
fn credentials_fn(&self) -> Option<&Arc<CredentialsFn>>;
|
||||||
|
|
||||||
|
/// Returns the URL of the index's repository
|
||||||
|
fn url(&self) -> &Url;
|
||||||
|
|
||||||
|
/// Returns the token to this index's registry
|
||||||
|
fn registry_auth_token(&self) -> Option<&str> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the index
|
||||||
|
fn refresh(&self) -> Result<(), RefreshError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns this as Any
|
||||||
|
fn as_any(&self) -> &dyn Any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A function that gets the credentials for a git repository
|
/// A function that gets the credentials for a git repository
|
||||||
|
@ -64,7 +82,8 @@ pub type CredentialsFn = Box<
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct GitIndex {
|
pub struct GitIndex {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
repo_url: String,
|
repo_url: Url,
|
||||||
|
registry_auth_token: Option<String>,
|
||||||
pub(crate) credentials_fn: Option<Arc<CredentialsFn>>,
|
pub(crate) credentials_fn: Option<Arc<CredentialsFn>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,6 +193,10 @@ pub enum IndexPackageError {
|
||||||
/// An error that occurred while deserializing the index file
|
/// An error that occurred while deserializing the index file
|
||||||
#[error("error deserializing index file")]
|
#[error("error deserializing index file")]
|
||||||
FileDeser(#[source] serde_yaml::Error),
|
FileDeser(#[source] serde_yaml::Error),
|
||||||
|
|
||||||
|
/// An unknown error occurred
|
||||||
|
#[error("unknown error")]
|
||||||
|
Other(#[source] Box<dyn std::error::Error + Send + Sync>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An error that occurred while creating a package version
|
/// An error that occurred while creating a package version
|
||||||
|
@ -202,6 +225,10 @@ pub enum CreatePackageVersionError {
|
||||||
/// The scope is missing ownership
|
/// The scope is missing ownership
|
||||||
#[error("missing scope ownership")]
|
#[error("missing scope ownership")]
|
||||||
MissingScopeOwnership,
|
MissingScopeOwnership,
|
||||||
|
|
||||||
|
/// An error that occurred while converting a manifest to an index file entry
|
||||||
|
#[error("error converting manifest to index file entry")]
|
||||||
|
FromManifestIndexFileEntry(#[from] FromManifestIndexFileEntry),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An error that occurred while getting the index's configuration
|
/// An error that occurred while getting the index's configuration
|
||||||
|
@ -247,29 +274,36 @@ fn get_refspec(
|
||||||
Ok((refspec.to_string(), upstream_branch.to_string()))
|
Ok((refspec.to_string(), upstream_branch.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn remote_callbacks<I: Index>(index: &I) -> git2::RemoteCallbacks {
|
macro_rules! remote_callbacks {
|
||||||
let mut remote_callbacks = git2::RemoteCallbacks::new();
|
($index:expr) => {{
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use crate::index::Index;
|
||||||
|
let mut remote_callbacks = git2::RemoteCallbacks::new();
|
||||||
|
|
||||||
if let Some(credentials) = &index.credentials_fn() {
|
if let Some(credentials) = &$index.credentials_fn() {
|
||||||
let credentials = std::sync::Arc::clone(credentials);
|
let credentials = std::sync::Arc::clone(credentials);
|
||||||
|
|
||||||
remote_callbacks.credentials(move |a, b, c| credentials()(a, b, c));
|
remote_callbacks.credentials(move |a, b, c| credentials()(a, b, c));
|
||||||
}
|
}
|
||||||
|
|
||||||
remote_callbacks
|
remote_callbacks
|
||||||
|
}};
|
||||||
}
|
}
|
||||||
|
pub(crate) use remote_callbacks;
|
||||||
|
|
||||||
impl GitIndex {
|
impl GitIndex {
|
||||||
/// Creates a new git index. The `refresh` method must be called before using the index, preferably immediately after creating it.
|
/// Creates a new git index. The `refresh` method must be called before using the index, preferably immediately after creating it.
|
||||||
pub fn new<P: AsRef<Path>>(
|
pub fn new<P: AsRef<Path>>(
|
||||||
path: P,
|
path: P,
|
||||||
repo_url: &str,
|
repo_url: &Url,
|
||||||
credentials: Option<CredentialsFn>,
|
credentials: Option<CredentialsFn>,
|
||||||
|
registry_auth_token: Option<String>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
path: path.as_ref().to_path_buf(),
|
path: path.as_ref().to_path_buf(),
|
||||||
repo_url: repo_url.to_string(),
|
repo_url: repo_url.clone(),
|
||||||
credentials_fn: credentials.map(Arc::new),
|
credentials_fn: credentials.map(Arc::new),
|
||||||
|
registry_auth_token,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -278,58 +312,6 @@ impl GitIndex {
|
||||||
&self.path
|
&self.path
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the URL of the index's repository
|
|
||||||
pub fn repo_url(&self) -> &str {
|
|
||||||
&self.repo_url
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Refreshes the index
|
|
||||||
pub fn refresh(&self) -> Result<(), RefreshError> {
|
|
||||||
let repo = if self.path.exists() {
|
|
||||||
Repository::open(&self.path).ok()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(repo) = repo {
|
|
||||||
let mut remote = repo.find_remote("origin")?;
|
|
||||||
let (refspec, upstream_branch) = get_refspec(&repo, &mut remote)?;
|
|
||||||
|
|
||||||
remote.fetch(
|
|
||||||
&[&refspec],
|
|
||||||
Some(git2::FetchOptions::new().remote_callbacks(remote_callbacks(self))),
|
|
||||||
None,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let commit = repo.find_reference(&upstream_branch)?.peel_to_commit()?;
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
"refreshing index, fetching {refspec}#{} from origin",
|
|
||||||
commit.id().to_string()
|
|
||||||
);
|
|
||||||
|
|
||||||
repo.reset(&commit.into_object(), git2::ResetType::Hard, None)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
debug!(
|
|
||||||
"refreshing index - first time, cloning {} into {}",
|
|
||||||
self.repo_url,
|
|
||||||
self.path.display()
|
|
||||||
);
|
|
||||||
create_dir_all(&self.path)?;
|
|
||||||
|
|
||||||
let mut fetch_options = git2::FetchOptions::new();
|
|
||||||
fetch_options.remote_callbacks(remote_callbacks(self));
|
|
||||||
|
|
||||||
RepoBuilder::new()
|
|
||||||
.fetch_options(fetch_options)
|
|
||||||
.clone(&self.repo_url, &self.path)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Commits and pushes to the index
|
/// Commits and pushes to the index
|
||||||
pub fn commit_and_push(
|
pub fn commit_and_push(
|
||||||
&self,
|
&self,
|
||||||
|
@ -362,13 +344,61 @@ impl GitIndex {
|
||||||
|
|
||||||
remote.push(
|
remote.push(
|
||||||
&[&refspec],
|
&[&refspec],
|
||||||
Some(git2::PushOptions::new().remote_callbacks(remote_callbacks(self))),
|
Some(git2::PushOptions::new().remote_callbacks(remote_callbacks!(self))),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! refresh_git_based_index {
|
||||||
|
($index:expr) => {{
|
||||||
|
let repo = if $index.path.exists() {
|
||||||
|
Repository::open(&$index.path).ok()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(repo) = repo {
|
||||||
|
let mut remote = repo.find_remote("origin")?;
|
||||||
|
let (refspec, upstream_branch) = get_refspec(&repo, &mut remote)?;
|
||||||
|
|
||||||
|
remote.fetch(
|
||||||
|
&[&refspec],
|
||||||
|
Some(git2::FetchOptions::new().remote_callbacks(remote_callbacks!($index))),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let commit = repo.find_reference(&upstream_branch)?.peel_to_commit()?;
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"refreshing index, fetching {refspec}#{} from origin",
|
||||||
|
commit.id().to_string()
|
||||||
|
);
|
||||||
|
|
||||||
|
repo.reset(&commit.into_object(), git2::ResetType::Hard, None)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
debug!(
|
||||||
|
"refreshing index - first time, cloning {} into {}",
|
||||||
|
$index.repo_url,
|
||||||
|
$index.path.display()
|
||||||
|
);
|
||||||
|
create_dir_all(&$index.path)?;
|
||||||
|
|
||||||
|
let mut fetch_options = git2::FetchOptions::new();
|
||||||
|
fetch_options.remote_callbacks(remote_callbacks!($index));
|
||||||
|
|
||||||
|
RepoBuilder::new()
|
||||||
|
.fetch_options(fetch_options)
|
||||||
|
.clone(&$index.repo_url.to_string(), &$index.path)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
impl Index for GitIndex {
|
impl Index for GitIndex {
|
||||||
fn scope_owners(&self, scope: &str) -> Result<Option<ScopeOwners>, ScopeOwnersError> {
|
fn scope_owners(&self, scope: &str) -> Result<Option<ScopeOwners>, ScopeOwnersError> {
|
||||||
let path = self.path.join(scope).join("owners.yaml");
|
let path = self.path.join(scope).join("owners.yaml");
|
||||||
|
@ -434,16 +464,17 @@ impl Index for GitIndex {
|
||||||
|
|
||||||
let path = self.path.join(scope);
|
let path = self.path.join(scope);
|
||||||
|
|
||||||
let mut file = if let Some(file) = self.package(&manifest.name)? {
|
let mut file =
|
||||||
if file.iter().any(|e| e.version == manifest.version) {
|
if let Some(file) = self.package(&PackageName::Standard(manifest.name.clone()))? {
|
||||||
return Ok(None);
|
if file.iter().any(|e| e.version == manifest.version) {
|
||||||
}
|
return Ok(None);
|
||||||
file
|
}
|
||||||
} else {
|
file
|
||||||
BTreeSet::new()
|
} else {
|
||||||
};
|
BTreeSet::new()
|
||||||
|
};
|
||||||
|
|
||||||
let entry: IndexFileEntry = manifest.clone().into();
|
let entry: IndexFileEntry = manifest.clone().try_into()?;
|
||||||
file.insert(entry.clone());
|
file.insert(entry.clone());
|
||||||
|
|
||||||
serde_yaml::to_writer(
|
serde_yaml::to_writer(
|
||||||
|
@ -472,6 +503,22 @@ impl Index for GitIndex {
|
||||||
fn credentials_fn(&self) -> Option<&Arc<CredentialsFn>> {
|
fn credentials_fn(&self) -> Option<&Arc<CredentialsFn>> {
|
||||||
self.credentials_fn.as_ref()
|
self.credentials_fn.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn url(&self) -> &Url {
|
||||||
|
&self.repo_url
|
||||||
|
}
|
||||||
|
|
||||||
|
fn registry_auth_token(&self) -> Option<&str> {
|
||||||
|
self.registry_auth_token.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh(&self) -> Result<(), RefreshError> {
|
||||||
|
refresh_git_based_index!(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The configuration of the index
|
/// The configuration of the index
|
||||||
|
@ -479,10 +526,10 @@ impl Index for GitIndex {
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct IndexConfig {
|
pub struct IndexConfig {
|
||||||
/// The URL of the index's API
|
/// The URL of the index's API
|
||||||
pub api: String,
|
pub api: Url,
|
||||||
/// The URL of the index's download API, defaults to `{API_URL}/v0/packages/{PACKAGE_AUTHOR}/{PACKAGE_NAME}/{PACKAGE_VERSION}`.
|
/// The URL of the index's download API, defaults to `{API_URL}/v0/packages/{PACKAGE_AUTHOR}/{PACKAGE_NAME}/{PACKAGE_VERSION}`.
|
||||||
/// Has the following variables:
|
/// Has the following variables:
|
||||||
/// - `{API_URL}`: The URL of the index's API
|
/// - `{API_URL}`: The URL of the index's API (without trailing `/`)
|
||||||
/// - `{PACKAGE_AUTHOR}`: The author of the package
|
/// - `{PACKAGE_AUTHOR}`: The author of the package
|
||||||
/// - `{PACKAGE_NAME}`: The name of the package
|
/// - `{PACKAGE_NAME}`: The name of the package
|
||||||
/// - `{PACKAGE_VERSION}`: The version of the package
|
/// - `{PACKAGE_VERSION}`: The version of the package
|
||||||
|
@ -500,7 +547,7 @@ pub struct IndexConfig {
|
||||||
impl IndexConfig {
|
impl IndexConfig {
|
||||||
/// Gets the URL of the index's API
|
/// Gets the URL of the index's API
|
||||||
pub fn api(&self) -> &str {
|
pub fn api(&self) -> &str {
|
||||||
self.api.strip_suffix('/').unwrap_or(&self.api)
|
self.api.as_str().trim_end_matches('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the URL of the index's download API
|
/// Gets the URL of the index's download API
|
||||||
|
@ -535,19 +582,48 @@ pub struct IndexFileEntry {
|
||||||
pub dependencies: Vec<(DependencySpecifier, DependencyType)>,
|
pub dependencies: Vec<(DependencySpecifier, DependencyType)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Manifest> for IndexFileEntry {
|
/// An error that occurred while converting a manifest to an index file entry
|
||||||
fn from(manifest: Manifest) -> IndexFileEntry {
|
#[derive(Debug, Error)]
|
||||||
let dependencies = manifest.dependencies();
|
pub enum FromManifestIndexFileEntry {
|
||||||
|
/// An error that occurred because an index is not specified
|
||||||
|
#[error("index {0} is not specified")]
|
||||||
|
IndexNotSpecified(String),
|
||||||
|
}
|
||||||
|
|
||||||
IndexFileEntry {
|
impl TryFrom<Manifest> for IndexFileEntry {
|
||||||
|
type Error = FromManifestIndexFileEntry;
|
||||||
|
|
||||||
|
fn try_from(manifest: Manifest) -> Result<Self, Self::Error> {
|
||||||
|
let dependencies = manifest.dependencies();
|
||||||
|
let indices = manifest.indices;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
version: manifest.version,
|
version: manifest.version,
|
||||||
realm: manifest.realm,
|
realm: manifest.realm,
|
||||||
published_at: Utc::now(),
|
published_at: Utc::now(),
|
||||||
|
|
||||||
description: manifest.description,
|
description: manifest.description,
|
||||||
|
|
||||||
dependencies,
|
dependencies: dependencies
|
||||||
}
|
.into_iter()
|
||||||
|
.map(|(dep, ty)| {
|
||||||
|
Ok(match dep {
|
||||||
|
DependencySpecifier::Registry(mut registry) => {
|
||||||
|
registry.index = indices
|
||||||
|
.get(®istry.index)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
FromManifestIndexFileEntry::IndexNotSpecified(
|
||||||
|
registry.index.clone(),
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.clone();
|
||||||
|
(DependencySpecifier::Registry(registry), ty)
|
||||||
|
}
|
||||||
|
d => (d, ty),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<_, _>>()?,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -565,3 +641,110 @@ impl Ord for IndexFileEntry {
|
||||||
|
|
||||||
/// An index file
|
/// An index file
|
||||||
pub type IndexFile = BTreeSet<IndexFileEntry>;
|
pub type IndexFile = BTreeSet<IndexFileEntry>;
|
||||||
|
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct WallyIndex {
|
||||||
|
repo_url: Url,
|
||||||
|
registry_auth_token: Option<String>,
|
||||||
|
credentials_fn: Option<Arc<CredentialsFn>>,
|
||||||
|
pub(crate) path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
impl Debug for WallyIndex {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("WallyIndex")
|
||||||
|
.field("path", &self.path)
|
||||||
|
.field("repo_url", &self.repo_url)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
impl WallyIndex {
|
||||||
|
pub(crate) fn new(
|
||||||
|
repo_url: Url,
|
||||||
|
registry_auth_token: Option<String>,
|
||||||
|
path: &Path,
|
||||||
|
credentials_fn: Option<Arc<CredentialsFn>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
repo_url,
|
||||||
|
registry_auth_token,
|
||||||
|
path: path.to_path_buf(),
|
||||||
|
credentials_fn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
impl Index for WallyIndex {
|
||||||
|
fn scope_owners(&self, _scope: &str) -> Result<Option<ScopeOwners>, ScopeOwnersError> {
|
||||||
|
unimplemented!("wally index is a virtual index meant for wally compatibility only")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_scope_for(
|
||||||
|
&mut self,
|
||||||
|
_scope: &str,
|
||||||
|
_owners: &ScopeOwners,
|
||||||
|
) -> Result<bool, ScopeOwnersError> {
|
||||||
|
unimplemented!("wally index is a virtual index meant for wally compatibility only")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn package(&self, name: &PackageName) -> Result<Option<IndexFile>, IndexPackageError> {
|
||||||
|
let path = self.path.join(name.scope()).join(name.name());
|
||||||
|
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = std::fs::File::open(&path)?;
|
||||||
|
let file = std::io::BufReader::new(file);
|
||||||
|
|
||||||
|
let manifest_stream = serde_json::Deserializer::from_reader(file)
|
||||||
|
.into_iter::<crate::dependencies::wally::WallyManifest>()
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(|e| IndexPackageError::Other(Box::new(e)))?;
|
||||||
|
|
||||||
|
Ok(Some(BTreeSet::from_iter(
|
||||||
|
manifest_stream
|
||||||
|
.into_iter()
|
||||||
|
.map(|m| m.try_into())
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(|e| IndexPackageError::Other(Box::new(e)))?,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_package_version(
|
||||||
|
&mut self,
|
||||||
|
_manifest: &Manifest,
|
||||||
|
_uploader: &u64,
|
||||||
|
) -> Result<Option<IndexFileEntry>, CreatePackageVersionError> {
|
||||||
|
unimplemented!("wally index is a virtual index meant for wally compatibility only")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config(&self) -> Result<IndexConfig, ConfigError> {
|
||||||
|
unimplemented!("wally index is a virtual index meant for wally compatibility only")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn credentials_fn(&self) -> Option<&Arc<CredentialsFn>> {
|
||||||
|
self.credentials_fn.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn url(&self) -> &Url {
|
||||||
|
&self.repo_url
|
||||||
|
}
|
||||||
|
|
||||||
|
fn registry_auth_token(&self) -> Option<&str> {
|
||||||
|
self.registry_auth_token.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh(&self) -> Result<(), RefreshError> {
|
||||||
|
refresh_git_based_index!(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
//! - Re-exporting types
|
//! - Re-exporting types
|
||||||
//! - `bin` exports (ran with Lune)
|
//! - `bin` exports (ran with Lune)
|
||||||
//! - Patching packages
|
//! - Patching packages
|
||||||
|
//! - Downloading packages from Wally registries
|
||||||
|
|
||||||
/// Resolving, downloading and managing dependencies
|
/// Resolving, downloading and managing dependencies
|
||||||
pub mod dependencies;
|
pub mod dependencies;
|
||||||
|
@ -44,5 +45,3 @@ pub const IGNORED_FOLDERS: &[&str] = &[
|
||||||
SERVER_PACKAGES_FOLDER,
|
SERVER_PACKAGES_FOLDER,
|
||||||
".git",
|
".git",
|
||||||
];
|
];
|
||||||
|
|
||||||
const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
use std::{
|
use std::{
|
||||||
fs::{read, write},
|
fs::{read_to_string, write},
|
||||||
iter,
|
iter,
|
||||||
path::{Component, Path, PathBuf},
|
path::{Component, Path, PathBuf},
|
||||||
str::from_utf8,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use full_moon::{
|
use full_moon::{
|
||||||
|
@ -16,7 +15,6 @@ use thiserror::Error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
dependencies::resolution::{packages_folder, ResolvedPackage, ResolvedVersionsMap},
|
dependencies::resolution::{packages_folder, ResolvedPackage, ResolvedVersionsMap},
|
||||||
index::Index,
|
|
||||||
manifest::{Manifest, ManifestReadError, PathStyle, Realm},
|
manifest::{Manifest, ManifestReadError, PathStyle, Realm},
|
||||||
package_name::PackageName,
|
package_name::PackageName,
|
||||||
project::Project,
|
project::Project,
|
||||||
|
@ -124,8 +122,8 @@ pub enum LinkingError {
|
||||||
InvalidLuau(#[from] full_moon::Error),
|
InvalidLuau(#[from] full_moon::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn link<P: AsRef<Path>, Q: AsRef<Path>, I: Index>(
|
pub(crate) fn link<P: AsRef<Path>, Q: AsRef<Path>>(
|
||||||
project: &Project<I>,
|
project: &Project,
|
||||||
resolved_pkg: &ResolvedPackage,
|
resolved_pkg: &ResolvedPackage,
|
||||||
destination_dir: P,
|
destination_dir: P,
|
||||||
parent_dependency_packages_dir: Q,
|
parent_dependency_packages_dir: Q,
|
||||||
|
@ -133,18 +131,19 @@ pub(crate) fn link<P: AsRef<Path>, Q: AsRef<Path>, I: Index>(
|
||||||
let (_, source_dir) = resolved_pkg.directory(project.path());
|
let (_, source_dir) = resolved_pkg.directory(project.path());
|
||||||
let file = Manifest::from_path(&source_dir)?;
|
let file = Manifest::from_path(&source_dir)?;
|
||||||
|
|
||||||
let Some(lib_export) = file.exports.lib else {
|
let Some(relative_lib_export) = file.exports.lib else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
let lib_export = lib_export.to_path(&source_dir);
|
let lib_export = relative_lib_export.to_path(&source_dir);
|
||||||
|
|
||||||
let path_style = &project.manifest().path_style;
|
let path_style = &project.manifest().path_style;
|
||||||
let PathStyle::Roblox { place } = &path_style;
|
let PathStyle::Roblox { place } = &path_style;
|
||||||
|
|
||||||
debug!("linking {resolved_pkg} using `{}` path style", path_style);
|
debug!("linking {resolved_pkg} using `{}` path style", path_style);
|
||||||
|
|
||||||
let name = resolved_pkg.pkg_ref.name().name();
|
let pkg_name = resolved_pkg.pkg_ref.name();
|
||||||
|
let name = pkg_name.name();
|
||||||
|
|
||||||
let destination_dir = if resolved_pkg.is_root {
|
let destination_dir = if resolved_pkg.is_root {
|
||||||
project.path().join(packages_folder(
|
project.path().join(packages_folder(
|
||||||
|
@ -154,7 +153,7 @@ pub(crate) fn link<P: AsRef<Path>, Q: AsRef<Path>, I: Index>(
|
||||||
destination_dir.as_ref().to_path_buf()
|
destination_dir.as_ref().to_path_buf()
|
||||||
};
|
};
|
||||||
|
|
||||||
let destination_file = destination_dir.join(format!("{name}.lua"));
|
let destination_file = destination_dir.join(format!("{}{}.lua", pkg_name.prefix(), name));
|
||||||
|
|
||||||
let realm_folder = project.path().join(resolved_pkg.packages_folder());
|
let realm_folder = project.path().join(resolved_pkg.packages_folder());
|
||||||
let in_different_folders = realm_folder != parent_dependency_packages_dir.as_ref();
|
let in_different_folders = realm_folder != parent_dependency_packages_dir.as_ref();
|
||||||
|
@ -199,10 +198,12 @@ pub(crate) fn link<P: AsRef<Path>, Q: AsRef<Path>, I: Index>(
|
||||||
destination_file.display()
|
destination_file.display()
|
||||||
);
|
);
|
||||||
|
|
||||||
let raw_file_contents = read(lib_export)?;
|
let file_contents = match relative_lib_export.as_str() {
|
||||||
let file_contents = from_utf8(&raw_file_contents)?;
|
"true" => "".to_string(),
|
||||||
|
_ => read_to_string(lib_export)?,
|
||||||
|
};
|
||||||
|
|
||||||
let linking_file_contents = linking_file(file_contents, &path)?;
|
let linking_file_contents = linking_file(&file_contents, &path)?;
|
||||||
|
|
||||||
write(&destination_file, linking_file_contents)?;
|
write(&destination_file, linking_file_contents)?;
|
||||||
|
|
||||||
|
@ -220,7 +221,7 @@ pub struct LinkingDependenciesError(
|
||||||
Version,
|
Version,
|
||||||
);
|
);
|
||||||
|
|
||||||
impl<I: Index> Project<I> {
|
impl Project {
|
||||||
/// Links the dependencies of the project
|
/// Links the dependencies of the project
|
||||||
pub fn link_dependencies(
|
pub fn link_dependencies(
|
||||||
&self,
|
&self,
|
||||||
|
|
190
src/manifest.rs
190
src/manifest.rs
|
@ -1,15 +1,14 @@
|
||||||
use std::fs::read_to_string;
|
use cfg_if::cfg_if;
|
||||||
use std::path::PathBuf;
|
use std::{collections::BTreeMap, fmt::Display, fs::read, str::FromStr};
|
||||||
use std::str::FromStr;
|
|
||||||
use std::{collections::BTreeMap, fmt::Display, fs::read};
|
|
||||||
|
|
||||||
use relative_path::RelativePathBuf;
|
use relative_path::RelativePathBuf;
|
||||||
use semver::{Version, VersionReq};
|
use semver::Version;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::dependencies::registry::RegistryDependencySpecifier;
|
use crate::{
|
||||||
use crate::{dependencies::DependencySpecifier, package_name::PackageName, MANIFEST_FILE_NAME};
|
dependencies::DependencySpecifier, package_name::StandardPackageName, MANIFEST_FILE_NAME,
|
||||||
|
};
|
||||||
|
|
||||||
/// The files exported by the package
|
/// The files exported by the package
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||||
|
@ -18,7 +17,7 @@ pub struct Exports {
|
||||||
/// Points to the file which exports the package. As of currently this is only used for re-exporting types.
|
/// Points to the file which exports the package. As of currently this is only used for re-exporting types.
|
||||||
/// Libraries must have a structure in Roblox where the main file becomes the folder, for example:
|
/// Libraries must have a structure in Roblox where the main file becomes the folder, for example:
|
||||||
/// A package called pesde/lib has a file called src/main.lua.
|
/// A package called pesde/lib has a file called src/main.lua.
|
||||||
/// Pesde puts this package in a folder called pesde_lib.
|
/// pesde puts this package in a folder called pesde_lib.
|
||||||
/// The package has to have set up configuration for file-syncing tools such as Rojo so that src/main.lua becomes the pesde_lib and turns it into a ModuleScript
|
/// The package has to have set up configuration for file-syncing tools such as Rojo so that src/main.lua becomes the pesde_lib and turns it into a ModuleScript
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub lib: Option<RelativePathBuf>,
|
pub lib: Option<RelativePathBuf>,
|
||||||
|
@ -114,7 +113,7 @@ impl FromStr for Realm {
|
||||||
// #[serde(deny_unknown_fields)]
|
// #[serde(deny_unknown_fields)]
|
||||||
pub struct Manifest {
|
pub struct Manifest {
|
||||||
/// The name of the package
|
/// The name of the package
|
||||||
pub name: PackageName,
|
pub name: StandardPackageName,
|
||||||
/// The version of the package. Must be [semver](https://semver.org) compatible. The registry will not accept non-semver versions and the CLI will not handle such packages
|
/// The version of the package. Must be [semver](https://semver.org) compatible. The registry will not accept non-semver versions and the CLI will not handle such packages
|
||||||
pub version: Version,
|
pub version: Version,
|
||||||
/// The files exported by the package
|
/// The files exported by the package
|
||||||
|
@ -128,6 +127,8 @@ pub struct Manifest {
|
||||||
pub private: bool,
|
pub private: bool,
|
||||||
/// The realm of the package
|
/// The realm of the package
|
||||||
pub realm: Option<Realm>,
|
pub realm: Option<Realm>,
|
||||||
|
/// Indices of the package
|
||||||
|
pub indices: BTreeMap<String, String>,
|
||||||
|
|
||||||
/// The dependencies of the package
|
/// The dependencies of the package
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
@ -162,40 +163,48 @@ pub enum ManifestReadError {
|
||||||
ManifestDeser(#[source] serde_yaml::Error),
|
ManifestDeser(#[source] serde_yaml::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An error that occurred while converting the manifest
|
cfg_if! {
|
||||||
#[derive(Debug, Error)]
|
if #[cfg(feature = "wally")] {
|
||||||
pub enum ManifestConvertError {
|
/// An error that occurred while converting the manifest
|
||||||
/// An error that occurred while reading the manifest
|
#[derive(Debug, Error)]
|
||||||
#[error("error reading the manifest")]
|
pub enum ManifestConvertError {
|
||||||
ManifestRead(#[from] ManifestReadError),
|
/// An error that occurred while reading the manifest
|
||||||
|
#[error("error reading the manifest")]
|
||||||
|
ManifestRead(#[from] ManifestReadError),
|
||||||
|
|
||||||
/// An error that occurred while converting the manifest
|
/// An error that occurred while converting the manifest
|
||||||
#[error("error converting the manifest")]
|
#[error("error converting the manifest")]
|
||||||
ManifestConvert(#[source] toml::de::Error),
|
ManifestConvert(#[source] toml::de::Error),
|
||||||
|
|
||||||
/// The given path does not have a parent
|
/// The given path does not have a parent
|
||||||
#[error("the path {0} does not have a parent")]
|
#[error("the path {0} does not have a parent")]
|
||||||
NoParent(PathBuf),
|
NoParent(std::path::PathBuf),
|
||||||
|
|
||||||
/// An error that occurred while interacting with the file system
|
/// An error that occurred while interacting with the file system
|
||||||
#[error("error interacting with the file system")]
|
#[error("error interacting with the file system")]
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
/// An error that occurred while making a package name from a string
|
/// An error that occurred while making a package name from a string
|
||||||
#[error("error making a package name from a string")]
|
#[error("error making a package name from a string")]
|
||||||
PackageName(#[from] crate::package_name::FromStrPackageNameParseError),
|
PackageName(
|
||||||
|
#[from]
|
||||||
|
crate::package_name::FromStrPackageNameParseError<
|
||||||
|
crate::package_name::StandardPackageNameValidationError,
|
||||||
|
>,
|
||||||
|
),
|
||||||
|
|
||||||
/// An error that occurred while writing the manifest
|
/// An error that occurred while writing the manifest
|
||||||
#[error("error writing the manifest")]
|
#[error("error writing the manifest")]
|
||||||
ManifestWrite(#[from] serde_yaml::Error),
|
ManifestWrite(#[from] serde_yaml::Error),
|
||||||
|
|
||||||
/// An error that occurred while converting a dependency specifier's version
|
/// An error that occurred while parsing the dependencies
|
||||||
#[error("error converting a dependency specifier's version")]
|
#[error("error parsing the dependencies")]
|
||||||
Version(#[from] semver::Error),
|
DependencyParse(#[from] crate::dependencies::wally::WallyManifestDependencyError),
|
||||||
|
}
|
||||||
/// The dependency specifier isn't in the format of `scope/name@version`
|
} else {
|
||||||
#[error("the dependency specifier {0} isn't in the format of `scope/name@version`")]
|
/// An error that occurred while converting the manifest
|
||||||
InvalidDependencySpecifier(String),
|
pub type ManifestConvertError = ManifestReadError;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The type of dependency
|
/// The type of dependency
|
||||||
|
@ -227,6 +236,7 @@ impl Manifest {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tries to read the manifest from the given path, and if it fails, tries converting the `wally.toml` and writes a `pesde.yaml` in the same directory
|
/// Tries to read the manifest from the given path, and if it fails, tries converting the `wally.toml` and writes a `pesde.yaml` in the same directory
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
pub fn from_path_or_convert<P: AsRef<std::path::Path>>(
|
pub fn from_path_or_convert<P: AsRef<std::path::Path>>(
|
||||||
path: P,
|
path: P,
|
||||||
) -> Result<Self, ManifestConvertError> {
|
) -> Result<Self, ManifestConvertError> {
|
||||||
|
@ -240,69 +250,14 @@ impl Manifest {
|
||||||
};
|
};
|
||||||
|
|
||||||
Self::from_path(path).or_else(|_| {
|
Self::from_path(path).or_else(|_| {
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct WallyPackage {
|
|
||||||
name: String,
|
|
||||||
version: Version,
|
|
||||||
#[serde(default)]
|
|
||||||
realm: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
description: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
license: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
authors: Option<Vec<String>>,
|
|
||||||
#[serde(default)]
|
|
||||||
private: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Default)]
|
|
||||||
struct WallyPlace {
|
|
||||||
#[serde(default)]
|
|
||||||
shared_packages: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
server_packages: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct WallyDependencySpecifier(String);
|
|
||||||
|
|
||||||
impl TryFrom<WallyDependencySpecifier> for DependencySpecifier {
|
|
||||||
type Error = ManifestConvertError;
|
|
||||||
|
|
||||||
fn try_from(specifier: WallyDependencySpecifier) -> Result<Self, Self::Error> {
|
|
||||||
let (name, req) = specifier.0.split_once('@').ok_or_else(|| {
|
|
||||||
ManifestConvertError::InvalidDependencySpecifier(specifier.0.clone())
|
|
||||||
})?;
|
|
||||||
let name: PackageName = name.replace('-', "_").parse()?;
|
|
||||||
let req: VersionReq = req.parse()?;
|
|
||||||
|
|
||||||
Ok(DependencySpecifier::Registry(RegistryDependencySpecifier {
|
|
||||||
name,
|
|
||||||
version: req,
|
|
||||||
realm: None,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct WallyManifest {
|
|
||||||
package: WallyPackage,
|
|
||||||
#[serde(default)]
|
|
||||||
place: WallyPlace,
|
|
||||||
#[serde(default)]
|
|
||||||
dependencies: BTreeMap<String, WallyDependencySpecifier>,
|
|
||||||
#[serde(default)]
|
|
||||||
server_dependencies: BTreeMap<String, WallyDependencySpecifier>,
|
|
||||||
#[serde(default)]
|
|
||||||
dev_dependencies: BTreeMap<String, WallyDependencySpecifier>,
|
|
||||||
}
|
|
||||||
|
|
||||||
let toml_path = dir_path.join("wally.toml");
|
let toml_path = dir_path.join("wally.toml");
|
||||||
let toml_contents = read_to_string(toml_path)?;
|
let toml_contents = std::fs::read_to_string(toml_path)?;
|
||||||
let wally_manifest: WallyManifest =
|
let wally_manifest: crate::dependencies::wally::WallyManifest =
|
||||||
toml::from_str(&toml_contents).map_err(ManifestConvertError::ManifestConvert)?;
|
toml::from_str(&toml_contents).map_err(ManifestConvertError::ManifestConvert)?;
|
||||||
|
|
||||||
|
let dependencies =
|
||||||
|
crate::dependencies::wally::parse_wally_dependencies(wally_manifest.clone())?;
|
||||||
|
|
||||||
let mut place = BTreeMap::new();
|
let mut place = BTreeMap::new();
|
||||||
|
|
||||||
if let Some(shared) = wally_manifest.place.shared_packages {
|
if let Some(shared) = wally_manifest.place.shared_packages {
|
||||||
|
@ -320,36 +275,21 @@ impl Manifest {
|
||||||
let manifest = Self {
|
let manifest = Self {
|
||||||
name: wally_manifest.package.name.replace('-', "_").parse()?,
|
name: wally_manifest.package.name.replace('-', "_").parse()?,
|
||||||
version: wally_manifest.package.version,
|
version: wally_manifest.package.version,
|
||||||
exports: Exports::default(),
|
exports: Exports {
|
||||||
|
lib: Some(RelativePathBuf::from("true")),
|
||||||
|
bin: None,
|
||||||
|
},
|
||||||
path_style: PathStyle::Roblox { place },
|
path_style: PathStyle::Roblox { place },
|
||||||
private: wally_manifest.package.private.unwrap_or(false),
|
private: wally_manifest.package.private.unwrap_or(false),
|
||||||
realm: wally_manifest
|
realm: wally_manifest
|
||||||
.package
|
.package
|
||||||
.realm
|
.realm
|
||||||
.map(|r| r.parse().unwrap_or(Realm::Shared)),
|
.map(|r| r.parse().unwrap_or(Realm::Shared)),
|
||||||
dependencies: [
|
indices: BTreeMap::from([(
|
||||||
(wally_manifest.dependencies, Realm::Shared),
|
crate::project::DEFAULT_INDEX_NAME.to_string(),
|
||||||
(wally_manifest.server_dependencies, Realm::Server),
|
"".to_string(),
|
||||||
(wally_manifest.dev_dependencies, Realm::Development),
|
)]),
|
||||||
]
|
dependencies,
|
||||||
.into_iter()
|
|
||||||
.flat_map(|(deps, realm)| {
|
|
||||||
deps.into_values()
|
|
||||||
.map(|specifier| {
|
|
||||||
specifier.try_into().map(|mut specifier| {
|
|
||||||
match specifier {
|
|
||||||
DependencySpecifier::Registry(ref mut specifier) => {
|
|
||||||
specifier.realm = Some(realm);
|
|
||||||
}
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
|
|
||||||
specifier
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
})
|
|
||||||
.collect::<Result<_, _>>()?,
|
|
||||||
peer_dependencies: Vec::new(),
|
peer_dependencies: Vec::new(),
|
||||||
description: wally_manifest.package.description,
|
description: wally_manifest.package.description,
|
||||||
license: wally_manifest.package.license,
|
license: wally_manifest.package.license,
|
||||||
|
@ -364,6 +304,14 @@ impl Manifest {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Same as `from_path`, enable the `wally` feature to add support for converting `wally.toml` to `pesde.yaml`
|
||||||
|
#[cfg(not(feature = "wally"))]
|
||||||
|
pub fn from_path_or_convert<P: AsRef<std::path::Path>>(
|
||||||
|
path: P,
|
||||||
|
) -> Result<Self, ManifestReadError> {
|
||||||
|
Self::from_path(path)
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns all dependencies
|
/// Returns all dependencies
|
||||||
pub fn dependencies(&self) -> Vec<(DependencySpecifier, DependencyType)> {
|
pub fn dependencies(&self) -> Vec<(DependencySpecifier, DependencyType)> {
|
||||||
self.dependencies
|
self.dependencies
|
||||||
|
|
|
@ -1,15 +1,23 @@
|
||||||
use std::{fmt::Display, str::FromStr};
|
use std::{
|
||||||
|
fmt::Debug,
|
||||||
|
hash::Hash,
|
||||||
|
{fmt::Display, str::FromStr},
|
||||||
|
};
|
||||||
|
|
||||||
use serde::{de::Visitor, Deserialize, Serialize};
|
use cfg_if::cfg_if;
|
||||||
|
use serde::{
|
||||||
|
de::{IntoDeserializer, Visitor},
|
||||||
|
Deserialize, Serialize,
|
||||||
|
};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
/// A package name
|
/// A package name
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
pub struct PackageName(String, String);
|
pub struct StandardPackageName(String, String);
|
||||||
|
|
||||||
/// An error that occurred while validating a package name part (scope or name)
|
/// An error that occurred while validating a package name part (scope or name)
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum PackageNameValidationError {
|
pub enum StandardPackageNameValidationError {
|
||||||
/// The package name part is empty
|
/// The package name part is empty
|
||||||
#[error("package name part cannot be empty")]
|
#[error("package name part cannot be empty")]
|
||||||
EmptyPart,
|
EmptyPart,
|
||||||
|
@ -22,130 +30,386 @@ pub enum PackageNameValidationError {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validates a package name part (scope or name)
|
/// Validates a package name part (scope or name)
|
||||||
pub fn validate_part(part: &str) -> Result<(), PackageNameValidationError> {
|
pub fn validate_part(part: &str) -> Result<(), StandardPackageNameValidationError> {
|
||||||
if part.is_empty() {
|
if part.is_empty() {
|
||||||
return Err(PackageNameValidationError::EmptyPart);
|
return Err(StandardPackageNameValidationError::EmptyPart);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !part
|
if !part
|
||||||
.chars()
|
.chars()
|
||||||
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
|
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
|
||||||
{
|
{
|
||||||
return Err(PackageNameValidationError::InvalidPart(part.to_string()));
|
return Err(StandardPackageNameValidationError::InvalidPart(
|
||||||
|
part.to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if part.len() > 24 {
|
if part.len() > 24 {
|
||||||
return Err(PackageNameValidationError::PartTooLong(part.to_string()));
|
return Err(StandardPackageNameValidationError::PartTooLong(
|
||||||
|
part.to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
const SEPARATOR: char = '/';
|
/// A wally package name
|
||||||
const ESCAPED_SEPARATOR: char = '-';
|
#[cfg(feature = "wally")]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
|
pub struct WallyPackageName(String, String);
|
||||||
|
|
||||||
|
/// An error that occurred while validating a wally package name part (scope or name)
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum WallyPackageNameValidationError {
|
||||||
|
/// The package name part is empty
|
||||||
|
#[error("wally package name part cannot be empty")]
|
||||||
|
EmptyPart,
|
||||||
|
/// The package name part contains invalid characters (only lowercase ASCII characters, numbers, and dashes are allowed)
|
||||||
|
#[error("wally package name {0} part can only contain lowercase ASCII characters, numbers, and dashes")]
|
||||||
|
InvalidPart(String),
|
||||||
|
/// The package name part is too long (it cannot be longer than 64 characters)
|
||||||
|
#[error("wally package name {0} part cannot be longer than 64 characters")]
|
||||||
|
PartTooLong(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates a wally package name part (scope or name)
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
pub fn validate_wally_part(part: &str) -> Result<(), WallyPackageNameValidationError> {
|
||||||
|
if part.is_empty() {
|
||||||
|
return Err(WallyPackageNameValidationError::EmptyPart);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !part
|
||||||
|
.chars()
|
||||||
|
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
|
||||||
|
{
|
||||||
|
return Err(WallyPackageNameValidationError::InvalidPart(
|
||||||
|
part.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if part.len() > 64 {
|
||||||
|
return Err(WallyPackageNameValidationError::PartTooLong(
|
||||||
|
part.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// An error that occurred while parsing an escaped package name
|
/// An error that occurred while parsing an escaped package name
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum EscapedPackageNameError {
|
pub enum EscapedPackageNameError<E> {
|
||||||
|
/// This package name is missing a prefix
|
||||||
|
#[error("package name is missing prefix {0}")]
|
||||||
|
MissingPrefix(String),
|
||||||
|
|
||||||
/// This is not a valid escaped package name
|
/// This is not a valid escaped package name
|
||||||
#[error("package name is not in the format `scope{ESCAPED_SEPARATOR}name`")]
|
#[error("package name {0} is not in the format `scope{ESCAPED_SEPARATOR}name`")]
|
||||||
Invalid,
|
Invalid(String),
|
||||||
|
|
||||||
/// The package name is invalid
|
/// The package name is invalid
|
||||||
#[error("invalid package name")]
|
#[error("invalid package name")]
|
||||||
InvalidName(#[from] PackageNameValidationError),
|
InvalidName(#[from] E),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An error that occurred while parsing a package name
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum FromStrPackageNameParseError<E> {
|
||||||
|
/// This is not a valid package name
|
||||||
|
#[error("package name {0} is not in the format `scope{SEPARATOR}name`")]
|
||||||
|
Invalid(String),
|
||||||
|
|
||||||
|
/// The package name is invalid
|
||||||
|
#[error("invalid name part")]
|
||||||
|
InvalidPart(#[from] E),
|
||||||
|
}
|
||||||
|
|
||||||
|
const SEPARATOR: char = '/';
|
||||||
|
const ESCAPED_SEPARATOR: char = '+';
|
||||||
|
|
||||||
|
macro_rules! name_impl {
|
||||||
|
($Name:ident, $Error:ident, $Visitor:ident, $validate:expr, $prefix:expr) => {
|
||||||
|
impl $Name {
|
||||||
|
/// Creates a new package name
|
||||||
|
pub fn new(scope: &str, name: &str) -> Result<Self, $Error> {
|
||||||
|
$validate(scope)?;
|
||||||
|
$validate(name)?;
|
||||||
|
|
||||||
|
Ok(Self(scope.to_string(), name.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses an escaped package name
|
||||||
|
pub fn from_escaped(s: &str) -> Result<Self, EscapedPackageNameError<$Error>> {
|
||||||
|
if !s.starts_with($prefix) {
|
||||||
|
return Err(EscapedPackageNameError::MissingPrefix($prefix.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (scope, name) = &s[$prefix.len()..]
|
||||||
|
.split_once(ESCAPED_SEPARATOR)
|
||||||
|
.ok_or_else(|| EscapedPackageNameError::Invalid(s.to_string()))?;
|
||||||
|
Ok(Self::new(scope, name)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the scope of the package name
|
||||||
|
pub fn scope(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the name of the package name
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
&self.1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the escaped form (for use in file names, etc.) of the package name
|
||||||
|
pub fn escaped(&self) -> String {
|
||||||
|
format!("{}{}{ESCAPED_SEPARATOR}{}", $prefix, self.0, self.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the parts of the package name
|
||||||
|
pub fn parts(&self) -> (&str, &str) {
|
||||||
|
(&self.0, &self.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the prefix for this package name
|
||||||
|
pub fn prefix() -> &'static str {
|
||||||
|
$prefix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for $Name {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}{}{SEPARATOR}{}", $prefix, self.0, self.1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for $Name {
|
||||||
|
type Err = FromStrPackageNameParseError<$Error>;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let len = if s.starts_with($prefix) {
|
||||||
|
$prefix.len()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
let parts: Vec<&str> = s[len..].split(SEPARATOR).collect();
|
||||||
|
if parts.len() != 2 {
|
||||||
|
return Err(FromStrPackageNameParseError::Invalid(s.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok($Name::new(parts[0], parts[1])?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for $Name {
|
||||||
|
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
serializer.serialize_str(&self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for $Visitor {
|
||||||
|
type Value = $Name;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
formatter,
|
||||||
|
"a string in the format `{}scope{SEPARATOR}name`",
|
||||||
|
$prefix
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
|
||||||
|
v.parse().map_err(|e| E::custom(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for $Name {
|
||||||
|
fn deserialize<D: serde::Deserializer<'de>>(
|
||||||
|
deserializer: D,
|
||||||
|
) -> Result<$Name, D::Error> {
|
||||||
|
deserializer.deserialize_str($Visitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StandardPackageNameVisitor;
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
struct WallyPackageNameVisitor;
|
||||||
|
|
||||||
|
/// A package name
|
||||||
|
#[derive(Serialize, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum PackageName {
|
||||||
|
/// A standard package name
|
||||||
|
Standard(StandardPackageName),
|
||||||
|
/// A wally package name
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
Wally(WallyPackageName),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PackageName {
|
impl PackageName {
|
||||||
/// Creates a new package name
|
|
||||||
pub fn new(scope: &str, name: &str) -> Result<Self, PackageNameValidationError> {
|
|
||||||
validate_part(scope)?;
|
|
||||||
validate_part(name)?;
|
|
||||||
|
|
||||||
Ok(Self(scope.to_string(), name.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parses an escaped package name
|
|
||||||
pub fn from_escaped(s: &str) -> Result<Self, EscapedPackageNameError> {
|
|
||||||
let (scope, name) = s
|
|
||||||
.split_once(ESCAPED_SEPARATOR)
|
|
||||||
.ok_or(EscapedPackageNameError::Invalid)?;
|
|
||||||
Ok(Self::new(scope, name)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the scope of the package name
|
/// Gets the scope of the package name
|
||||||
pub fn scope(&self) -> &str {
|
pub fn scope(&self) -> &str {
|
||||||
&self.0
|
match self {
|
||||||
|
PackageName::Standard(name) => name.scope(),
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
PackageName::Wally(name) => name.scope(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the name of the package name
|
/// Gets the name of the package name
|
||||||
pub fn name(&self) -> &str {
|
pub fn name(&self) -> &str {
|
||||||
&self.1
|
match self {
|
||||||
|
PackageName::Standard(name) => name.name(),
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
PackageName::Wally(name) => name.name(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the escaped form (for use in file names, etc.) of the package name
|
/// Gets the escaped form (for use in file names, etc.) of the package name
|
||||||
pub fn escaped(&self) -> String {
|
pub fn escaped(&self) -> String {
|
||||||
format!("{}{ESCAPED_SEPARATOR}{}", self.0, self.1)
|
match self {
|
||||||
|
PackageName::Standard(name) => name.escaped(),
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
PackageName::Wally(name) => name.escaped(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the parts of the package name
|
/// Gets the parts of the package name
|
||||||
pub fn parts(&self) -> (&str, &str) {
|
pub fn parts(&self) -> (&str, &str) {
|
||||||
(&self.0, &self.1)
|
match self {
|
||||||
|
PackageName::Standard(name) => name.parts(),
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
PackageName::Wally(name) => name.parts(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the prefix for this package name
|
||||||
|
pub fn prefix(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
PackageName::Standard(_) => StandardPackageName::prefix(),
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
PackageName::Wally(_) => WallyPackageName::prefix(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for PackageName {
|
impl Display for PackageName {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "{}{SEPARATOR}{}", self.0, self.1)
|
match self {
|
||||||
|
PackageName::Standard(name) => write!(f, "{name}"),
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
PackageName::Wally(name) => write!(f, "{name}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StandardPackageName> for PackageName {
|
||||||
|
fn from(name: StandardPackageName) -> Self {
|
||||||
|
PackageName::Standard(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
impl From<WallyPackageName> for PackageName {
|
||||||
|
fn from(name: WallyPackageName) -> Self {
|
||||||
|
PackageName::Wally(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
name_impl!(
|
||||||
|
StandardPackageName,
|
||||||
|
StandardPackageNameValidationError,
|
||||||
|
StandardPackageNameVisitor,
|
||||||
|
validate_part,
|
||||||
|
""
|
||||||
|
);
|
||||||
|
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
name_impl!(
|
||||||
|
WallyPackageName,
|
||||||
|
WallyPackageNameValidationError,
|
||||||
|
WallyPackageNameVisitor,
|
||||||
|
validate_wally_part,
|
||||||
|
"wally#"
|
||||||
|
);
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for PackageName {
|
||||||
|
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
|
let s = String::deserialize(deserializer)?;
|
||||||
|
|
||||||
|
cfg_if! {
|
||||||
|
if #[cfg(feature = "wally")] {
|
||||||
|
if s.starts_with(WallyPackageName::prefix()) {
|
||||||
|
return Ok(PackageName::Wally(
|
||||||
|
WallyPackageName::deserialize(s.into_deserializer())?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(PackageName::Standard(StandardPackageName::deserialize(
|
||||||
|
s.into_deserializer(),
|
||||||
|
)?))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An error that occurred while parsing a package name
|
/// An error that occurred while parsing a package name
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum FromStrPackageNameParseError {
|
pub enum FromStrPackageNameError {
|
||||||
/// This is not a valid package name
|
/// Error parsing the package name as a standard package name
|
||||||
#[error("package name is not in the format `scope{SEPARATOR}name`")]
|
#[error("error parsing standard package name")]
|
||||||
Invalid,
|
Standard(#[from] FromStrPackageNameParseError<StandardPackageNameValidationError>),
|
||||||
/// The package name is invalid
|
|
||||||
#[error("invalid name part")]
|
/// Error parsing the package name as a wally package name
|
||||||
InvalidPart(#[from] PackageNameValidationError),
|
#[cfg(feature = "wally")]
|
||||||
|
#[error("error parsing wally package name")]
|
||||||
|
Wally(#[from] FromStrPackageNameParseError<WallyPackageNameValidationError>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for PackageName {
|
impl FromStr for PackageName {
|
||||||
type Err = FromStrPackageNameParseError;
|
type Err = FromStrPackageNameError;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
let parts: Vec<&str> = s.split(SEPARATOR).collect();
|
cfg_if! {
|
||||||
if parts.len() != 2 {
|
if #[cfg(feature = "wally")] {
|
||||||
return Err(FromStrPackageNameParseError::Invalid);
|
if s.starts_with(WallyPackageName::prefix()) {
|
||||||
|
return Ok(PackageName::Wally(WallyPackageName::from_str(s)?));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(PackageName::new(parts[0], parts[1])?)
|
Ok(PackageName::Standard(StandardPackageName::from_str(s)?))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Serialize for PackageName {
|
/// An error that occurred while parsing an escaped package name
|
||||||
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
#[derive(Debug, Error)]
|
||||||
serializer.serialize_str(&self.to_string())
|
pub enum FromEscapedStrPackageNameError {
|
||||||
}
|
/// Error parsing the package name as a standard package name
|
||||||
|
#[error("error parsing standard package name")]
|
||||||
|
Standard(#[from] EscapedPackageNameError<StandardPackageNameValidationError>),
|
||||||
|
|
||||||
|
/// Error parsing the package name as a wally package name
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
#[error("error parsing wally package name")]
|
||||||
|
Wally(#[from] EscapedPackageNameError<WallyPackageNameValidationError>),
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PackageNameVisitor;
|
impl PackageName {
|
||||||
|
/// Like `from_str`, but for escaped package names
|
||||||
|
pub fn from_escaped_str(s: &str) -> Result<Self, FromEscapedStrPackageNameError> {
|
||||||
|
cfg_if! {
|
||||||
|
if #[cfg(feature = "wally")] {
|
||||||
|
if s.starts_with(WallyPackageName::prefix()) {
|
||||||
|
return Ok(PackageName::Wally(WallyPackageName::from_escaped(s)?));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for PackageNameVisitor {
|
Ok(PackageName::Standard(StandardPackageName::from_escaped(s)?))
|
||||||
type Value = PackageName;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
write!(formatter, "a string in the format `scope{SEPARATOR}name`")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
|
|
||||||
v.parse().map_err(|e| E::custom(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for PackageName {
|
|
||||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<PackageName, D::Error> {
|
|
||||||
deserializer.deserialize_str(PackageNameVisitor)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,10 @@ use semver::Version;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
dependencies::resolution::ResolvedVersionsMap, index::Index, package_name::PackageName,
|
dependencies::resolution::ResolvedVersionsMap,
|
||||||
project::Project, PATCHES_FOLDER,
|
package_name::{FromEscapedStrPackageNameError, PackageName},
|
||||||
|
project::Project,
|
||||||
|
PATCHES_FOLDER,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn make_signature<'a>() -> Result<Signature<'a>, git2::Error> {
|
fn make_signature<'a>() -> Result<Signature<'a>, git2::Error> {
|
||||||
|
@ -106,11 +108,11 @@ pub enum ApplyPatchesError {
|
||||||
|
|
||||||
/// An error that occurred because a patch name was malformed
|
/// An error that occurred because a patch name was malformed
|
||||||
#[error("malformed patch name {0}")]
|
#[error("malformed patch name {0}")]
|
||||||
MalformedPatch(String),
|
MalformedPatchName(String),
|
||||||
|
|
||||||
/// An error that occurred while parsing a package name
|
/// An error that occurred while parsing a package name
|
||||||
#[error("failed to parse package name {0}")]
|
#[error("failed to parse package name {0}")]
|
||||||
PackageNameParse(#[from] crate::package_name::EscapedPackageNameError),
|
PackageNameParse(#[from] FromEscapedStrPackageNameError),
|
||||||
|
|
||||||
/// An error that occurred while getting a file stem
|
/// An error that occurred while getting a file stem
|
||||||
#[error("failed to get file stem")]
|
#[error("failed to get file stem")]
|
||||||
|
@ -137,7 +139,7 @@ pub enum ApplyPatchesError {
|
||||||
StripPrefixFail(#[from] std::path::StripPrefixError),
|
StripPrefixFail(#[from] std::path::StripPrefixError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<I: Index> Project<I> {
|
impl Project {
|
||||||
/// Applies patches for the project
|
/// Applies patches for the project
|
||||||
pub fn apply_patches(&self, map: &ResolvedVersionsMap) -> Result<(), ApplyPatchesError> {
|
pub fn apply_patches(&self, map: &ResolvedVersionsMap) -> Result<(), ApplyPatchesError> {
|
||||||
let patches_dir = self.path().join(PATCHES_FOLDER);
|
let patches_dir = self.path().join(PATCHES_FOLDER);
|
||||||
|
@ -153,27 +155,28 @@ impl<I: Index> Project<I> {
|
||||||
|
|
||||||
let path = file.path();
|
let path = file.path();
|
||||||
|
|
||||||
let dir_name = path
|
let file_name = path
|
||||||
.file_name()
|
.file_name()
|
||||||
.ok_or_else(|| ApplyPatchesError::FileNameFail(path.clone()))?;
|
.ok_or_else(|| ApplyPatchesError::FileNameFail(path.clone()))?;
|
||||||
let dir_name = dir_name.to_str().ok_or(ApplyPatchesError::ToStringFail)?;
|
let file_name = file_name.to_str().ok_or(ApplyPatchesError::ToStringFail)?;
|
||||||
|
|
||||||
let (package_name, version) = dir_name
|
let (package_name, version) = file_name
|
||||||
.strip_suffix(".patch")
|
.strip_suffix(".patch")
|
||||||
.unwrap_or(dir_name)
|
.unwrap_or(file_name)
|
||||||
.split_once('@')
|
.split_once('@')
|
||||||
.ok_or_else(|| ApplyPatchesError::MalformedPatch(dir_name.to_string()))?;
|
.ok_or_else(|| ApplyPatchesError::MalformedPatchName(file_name.to_string()))?;
|
||||||
|
|
||||||
|
let package_name = PackageName::from_escaped_str(package_name)?;
|
||||||
|
|
||||||
let package_name = PackageName::from_escaped(package_name)?;
|
|
||||||
let version = Version::parse(version)?;
|
let version = Version::parse(version)?;
|
||||||
|
|
||||||
let versions = map
|
let resolved_pkg = map
|
||||||
.get(&package_name)
|
.get(&package_name)
|
||||||
.ok_or_else(|| ApplyPatchesError::PackageNotFound(package_name.clone()))?;
|
.ok_or_else(|| ApplyPatchesError::PackageNotFound(package_name.clone()))?
|
||||||
|
.get(&version)
|
||||||
let resolved_pkg = versions.get(&version).ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
ApplyPatchesError::VersionNotFound(version.clone(), package_name.clone())
|
ApplyPatchesError::VersionNotFound(version.clone(), package_name.clone())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
debug!("resolved package {package_name}@{version} to {resolved_pkg}");
|
debug!("resolved package {package_name}@{version} to {resolved_pkg}");
|
||||||
|
|
||||||
|
|
177
src/project.rs
177
src/project.rs
|
@ -1,27 +1,33 @@
|
||||||
|
use log::{error, warn};
|
||||||
use std::{
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
fmt::Debug,
|
fmt::Debug,
|
||||||
fs::{read, File},
|
fs::{read, File},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
use crate::dependencies::DownloadError;
|
use thiserror::Error;
|
||||||
use crate::index::Index;
|
use url::Url;
|
||||||
use crate::linking_file::LinkingDependenciesError;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
dependencies::resolution::ResolvedVersionsMap,
|
dependencies::{resolution::ResolvedVersionsMap, DownloadError, UrlResolveError},
|
||||||
|
index::Index,
|
||||||
|
linking_file::LinkingDependenciesError,
|
||||||
manifest::{Manifest, ManifestReadError},
|
manifest::{Manifest, ManifestReadError},
|
||||||
LOCKFILE_FILE_NAME,
|
LOCKFILE_FILE_NAME,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// A map of indices
|
||||||
|
pub type Indices = HashMap<String, Box<dyn Index>>;
|
||||||
|
|
||||||
/// A pesde project
|
/// A pesde project
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Project<I: Index> {
|
pub struct Project {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
cache_path: PathBuf,
|
cache_path: PathBuf,
|
||||||
index: I,
|
indices: Indices,
|
||||||
manifest: Manifest,
|
manifest: Manifest,
|
||||||
registry_auth_token: Option<String>,
|
pub(crate) reqwest_client: reqwest::blocking::Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Options for installing a project
|
/// Options for installing a project
|
||||||
|
@ -114,47 +120,141 @@ pub enum InstallProjectError {
|
||||||
/// An error that occurred while writing the lockfile
|
/// An error that occurred while writing the lockfile
|
||||||
#[error("failed to write lockfile")]
|
#[error("failed to write lockfile")]
|
||||||
LockfileSer(#[source] serde_yaml::Error),
|
LockfileSer(#[source] serde_yaml::Error),
|
||||||
|
|
||||||
|
/// An error that occurred while resolving the url of a package
|
||||||
|
#[error("failed to resolve package URL")]
|
||||||
|
UrlResolve(#[from] UrlResolveError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<I: Index> Project<I> {
|
/// The name of the default index to use
|
||||||
|
pub const DEFAULT_INDEX_NAME: &str = "default";
|
||||||
|
|
||||||
|
pub(crate) fn get_index<'a>(indices: &'a Indices, index_name: Option<&str>) -> &'a dyn Index {
|
||||||
|
indices
|
||||||
|
.get(index_name.unwrap_or(DEFAULT_INDEX_NAME))
|
||||||
|
.or_else(|| {
|
||||||
|
warn!(
|
||||||
|
"index `{}` not found, using default index",
|
||||||
|
index_name.unwrap_or("<not provided>")
|
||||||
|
);
|
||||||
|
indices.get(DEFAULT_INDEX_NAME)
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_index_by_url<'a>(indices: &'a Indices, url: &Url) -> &'a dyn Index {
|
||||||
|
indices
|
||||||
|
.values()
|
||||||
|
.find(|index| index.url() == url)
|
||||||
|
.map(|index| index.as_ref())
|
||||||
|
.unwrap_or_else(|| get_index(indices, None))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
pub(crate) fn get_wally_index<'a>(
|
||||||
|
indices: &'a mut Indices,
|
||||||
|
url: &Url,
|
||||||
|
path: Option<&Path>,
|
||||||
|
) -> Result<&'a crate::index::WallyIndex, crate::index::RefreshError> {
|
||||||
|
if !indices.contains_key(url.as_str()) {
|
||||||
|
let default_index = indices.get(DEFAULT_INDEX_NAME).unwrap();
|
||||||
|
let default_token = default_index.registry_auth_token().map(|t| t.to_string());
|
||||||
|
let default_credentials_fn = default_index.credentials_fn().cloned();
|
||||||
|
|
||||||
|
let index = crate::index::WallyIndex::new(
|
||||||
|
url.clone(),
|
||||||
|
default_token,
|
||||||
|
path.expect("index should already exist by now"),
|
||||||
|
default_credentials_fn,
|
||||||
|
);
|
||||||
|
|
||||||
|
match index.refresh() {
|
||||||
|
Ok(_) => {
|
||||||
|
indices.insert(url.as_str().to_string(), Box::new(index));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("failed to refresh wally index: {e}");
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(indices
|
||||||
|
.get(url.as_str())
|
||||||
|
.unwrap()
|
||||||
|
.as_any()
|
||||||
|
.downcast_ref()
|
||||||
|
.unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An error that occurred while creating a new project
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum NewProjectError {
|
||||||
|
/// A default index was not provided
|
||||||
|
#[error("default index not provided")]
|
||||||
|
DefaultIndexNotProvided,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An error that occurred while creating a project from a path
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ProjectFromPathError {
|
||||||
|
/// An error that occurred while reading the manifest
|
||||||
|
#[error("error reading manifest")]
|
||||||
|
ManifestRead(#[from] ManifestReadError),
|
||||||
|
|
||||||
|
/// An error that occurred while creating the project
|
||||||
|
#[error("error creating project")]
|
||||||
|
NewProject(#[from] NewProjectError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Project {
|
||||||
/// Creates a new project
|
/// Creates a new project
|
||||||
pub fn new<P: AsRef<Path>, Q: AsRef<Path>>(
|
pub fn new<P: AsRef<Path>, Q: AsRef<Path>>(
|
||||||
path: P,
|
path: P,
|
||||||
cache_path: Q,
|
cache_path: Q,
|
||||||
index: I,
|
indices: Indices,
|
||||||
manifest: Manifest,
|
manifest: Manifest,
|
||||||
registry_auth_token: Option<String>,
|
) -> Result<Self, NewProjectError> {
|
||||||
) -> Self {
|
if !indices.contains_key(DEFAULT_INDEX_NAME) {
|
||||||
Self {
|
return Err(NewProjectError::DefaultIndexNotProvided);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
path: path.as_ref().to_path_buf(),
|
path: path.as_ref().to_path_buf(),
|
||||||
cache_path: cache_path.as_ref().to_path_buf(),
|
cache_path: cache_path.as_ref().to_path_buf(),
|
||||||
index,
|
indices,
|
||||||
manifest,
|
manifest,
|
||||||
registry_auth_token,
|
reqwest_client: reqwest::blocking::ClientBuilder::new()
|
||||||
}
|
.user_agent(concat!(
|
||||||
|
env!("CARGO_PKG_NAME"),
|
||||||
|
"/",
|
||||||
|
env!("CARGO_PKG_VERSION")
|
||||||
|
))
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new project from a path (manifest will be read from the path)
|
/// Creates a new project from a path (manifest will be read from the path)
|
||||||
pub fn from_path<P: AsRef<Path>, Q: AsRef<Path>>(
|
pub fn from_path<P: AsRef<Path>, Q: AsRef<Path>>(
|
||||||
path: P,
|
path: P,
|
||||||
cache_path: Q,
|
cache_path: Q,
|
||||||
index: I,
|
indices: Indices,
|
||||||
registry_auth_token: Option<String>,
|
) -> Result<Self, ProjectFromPathError> {
|
||||||
) -> Result<Self, ManifestReadError> {
|
|
||||||
let manifest = Manifest::from_path(path.as_ref())?;
|
let manifest = Manifest::from_path(path.as_ref())?;
|
||||||
|
|
||||||
Ok(Self::new(
|
Ok(Self::new(path, cache_path, indices, manifest)?)
|
||||||
path,
|
|
||||||
cache_path,
|
|
||||||
index,
|
|
||||||
manifest,
|
|
||||||
registry_auth_token,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the index of the project
|
/// Returns the indices of the project
|
||||||
pub fn index(&self) -> &I {
|
pub fn indices(&self) -> &HashMap<String, Box<dyn Index>> {
|
||||||
&self.index
|
&self.indices
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "wally")]
|
||||||
|
pub(crate) fn indices_mut(&mut self) -> &mut HashMap<String, Box<dyn Index>> {
|
||||||
|
&mut self.indices
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the manifest of the project
|
/// Returns the manifest of the project
|
||||||
|
@ -172,11 +272,6 @@ impl<I: Index> Project<I> {
|
||||||
&self.path
|
&self.path
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the registry auth token of the project
|
|
||||||
pub fn registry_auth_token(&self) -> Option<&String> {
|
|
||||||
self.registry_auth_token.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the lockfile of the project
|
/// Returns the lockfile of the project
|
||||||
pub fn lockfile(&self) -> Result<Option<ResolvedVersionsMap>, ReadLockfileError> {
|
pub fn lockfile(&self) -> Result<Option<ResolvedVersionsMap>, ReadLockfileError> {
|
||||||
let lockfile_path = self.path.join(LOCKFILE_FILE_NAME);
|
let lockfile_path = self.path.join(LOCKFILE_FILE_NAME);
|
||||||
|
@ -193,16 +288,18 @@ impl<I: Index> Project<I> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Downloads the project's dependencies, applies patches, and links the dependencies
|
/// Downloads the project's dependencies, applies patches, and links the dependencies
|
||||||
pub fn install(&self, install_options: InstallOptions) -> Result<(), InstallProjectError> {
|
pub fn install(&mut self, install_options: InstallOptions) -> Result<(), InstallProjectError> {
|
||||||
let map = match install_options.resolved_versions_map {
|
let map = match install_options.resolved_versions_map {
|
||||||
Some(map) => map,
|
Some(map) => map,
|
||||||
None => self
|
None => {
|
||||||
.manifest
|
let manifest = self.manifest.clone();
|
||||||
.dependency_tree(self, install_options.locked)?,
|
|
||||||
|
manifest.dependency_tree(self, install_options.locked)?
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if install_options.auto_download {
|
if install_options.auto_download {
|
||||||
self.download(&map)?.wait()?;
|
self.download(map.clone())?.wait()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.apply_patches(&map)?;
|
self.apply_patches(&map)?;
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
use std::{
|
use std::{
|
||||||
|
any::Any,
|
||||||
collections::{BTreeSet, HashMap},
|
collections::{BTreeSet, HashMap},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use pesde::{
|
use pesde::{
|
||||||
index::{
|
index::{
|
||||||
|
@ -13,9 +15,19 @@ use pesde::{
|
||||||
};
|
};
|
||||||
|
|
||||||
/// An in-memory implementation of the [`Index`] trait. Used for testing.
|
/// An in-memory implementation of the [`Index`] trait. Used for testing.
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct InMemoryIndex {
|
pub struct InMemoryIndex {
|
||||||
packages: HashMap<String, (BTreeSet<u64>, IndexFile)>,
|
packages: HashMap<String, (BTreeSet<u64>, IndexFile)>,
|
||||||
|
url: Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for InMemoryIndex {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
packages: HashMap::new(),
|
||||||
|
url: Url::parse("https://example.com").unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InMemoryIndex {
|
impl InMemoryIndex {
|
||||||
|
@ -78,7 +90,7 @@ impl Index for InMemoryIndex {
|
||||||
|
|
||||||
let package = self.packages.get_mut(scope).unwrap();
|
let package = self.packages.get_mut(scope).unwrap();
|
||||||
|
|
||||||
let entry: IndexFileEntry = manifest.clone().into();
|
let entry: IndexFileEntry = manifest.clone().try_into()?;
|
||||||
package.1.insert(entry.clone());
|
package.1.insert(entry.clone());
|
||||||
|
|
||||||
Ok(Some(entry))
|
Ok(Some(entry))
|
||||||
|
@ -87,7 +99,7 @@ impl Index for InMemoryIndex {
|
||||||
fn config(&self) -> Result<IndexConfig, ConfigError> {
|
fn config(&self) -> Result<IndexConfig, ConfigError> {
|
||||||
Ok(IndexConfig {
|
Ok(IndexConfig {
|
||||||
download: None,
|
download: None,
|
||||||
api: "http://127.0.0.1:8080".to_string(),
|
api: "http://127.0.0.1:8080".parse().unwrap(),
|
||||||
github_oauth_client_id: "".to_string(),
|
github_oauth_client_id: "".to_string(),
|
||||||
custom_registry_allowed: false,
|
custom_registry_allowed: false,
|
||||||
git_allowed: false,
|
git_allowed: false,
|
||||||
|
@ -97,4 +109,12 @@ impl Index for InMemoryIndex {
|
||||||
fn credentials_fn(&self) -> Option<&Arc<CredentialsFn>> {
|
fn credentials_fn(&self) -> Option<&Arc<CredentialsFn>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn url(&self) -> &Url {
|
||||||
|
&self.url
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::collections::BTreeSet;
|
use std::collections::{BTreeSet, HashMap};
|
||||||
|
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
@ -9,9 +9,10 @@ use pesde::{
|
||||||
resolution::ResolvedPackage,
|
resolution::ResolvedPackage,
|
||||||
DependencySpecifier, PackageRef,
|
DependencySpecifier, PackageRef,
|
||||||
},
|
},
|
||||||
|
index::Index,
|
||||||
manifest::{DependencyType, Manifest, Realm},
|
manifest::{DependencyType, Manifest, Realm},
|
||||||
package_name::PackageName,
|
package_name::StandardPackageName,
|
||||||
project::Project,
|
project::{Project, DEFAULT_INDEX_NAME},
|
||||||
};
|
};
|
||||||
use prelude::*;
|
use prelude::*;
|
||||||
|
|
||||||
|
@ -30,7 +31,7 @@ fn test_resolves_package() {
|
||||||
|
|
||||||
let description = "test package";
|
let description = "test package";
|
||||||
|
|
||||||
let pkg_name = PackageName::new("test", "test").unwrap();
|
let pkg_name = StandardPackageName::new("test", "test").unwrap();
|
||||||
|
|
||||||
let pkg_manifest = Manifest {
|
let pkg_manifest = Manifest {
|
||||||
name: pkg_name.clone(),
|
name: pkg_name.clone(),
|
||||||
|
@ -39,6 +40,7 @@ fn test_resolves_package() {
|
||||||
path_style: Default::default(),
|
path_style: Default::default(),
|
||||||
private: true,
|
private: true,
|
||||||
realm: None,
|
realm: None,
|
||||||
|
indices: Default::default(),
|
||||||
dependencies: vec![],
|
dependencies: vec![],
|
||||||
peer_dependencies: vec![],
|
peer_dependencies: vec![],
|
||||||
description: Some(description.to_string()),
|
description: Some(description.to_string()),
|
||||||
|
@ -52,18 +54,20 @@ fn test_resolves_package() {
|
||||||
|
|
||||||
let index = index
|
let index = index
|
||||||
.with_scope(pkg_name.scope(), BTreeSet::from([0]))
|
.with_scope(pkg_name.scope(), BTreeSet::from([0]))
|
||||||
.with_package(pkg_name.scope(), pkg_manifest.into())
|
.with_package(pkg_name.scope(), pkg_manifest.try_into().unwrap())
|
||||||
.with_package(pkg_name.scope(), pkg_2_manifest.into());
|
.with_package(pkg_name.scope(), pkg_2_manifest.try_into().unwrap());
|
||||||
|
|
||||||
let specifier = DependencySpecifier::Registry(RegistryDependencySpecifier {
|
let specifier = DependencySpecifier::Registry(RegistryDependencySpecifier {
|
||||||
name: pkg_name.clone(),
|
name: pkg_name.clone(),
|
||||||
version: format!("={version_str}").parse().unwrap(),
|
version: format!("={version_str}").parse().unwrap(),
|
||||||
realm: None,
|
realm: None,
|
||||||
|
index: DEFAULT_INDEX_NAME.to_string(),
|
||||||
});
|
});
|
||||||
let specifier_2 = DependencySpecifier::Registry(RegistryDependencySpecifier {
|
let specifier_2 = DependencySpecifier::Registry(RegistryDependencySpecifier {
|
||||||
name: pkg_name.clone(),
|
name: pkg_name.clone(),
|
||||||
version: format!(">{version_str}").parse().unwrap(),
|
version: format!(">{version_str}").parse().unwrap(),
|
||||||
realm: None,
|
realm: None,
|
||||||
|
index: DEFAULT_INDEX_NAME.to_string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let user_manifest = Manifest {
|
let user_manifest = Manifest {
|
||||||
|
@ -73,6 +77,7 @@ fn test_resolves_package() {
|
||||||
path_style: Default::default(),
|
path_style: Default::default(),
|
||||||
private: true,
|
private: true,
|
||||||
realm: None,
|
realm: None,
|
||||||
|
indices: Default::default(),
|
||||||
dependencies: vec![specifier.clone()],
|
dependencies: vec![specifier.clone()],
|
||||||
peer_dependencies: vec![specifier_2.clone()],
|
peer_dependencies: vec![specifier_2.clone()],
|
||||||
description: Some(description.to_string()),
|
description: Some(description.to_string()),
|
||||||
|
@ -81,11 +86,21 @@ fn test_resolves_package() {
|
||||||
repository: None,
|
repository: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let project = Project::new(&dir_path, &dir_path, index, user_manifest, None);
|
let mut project = Project::new(
|
||||||
|
&dir_path,
|
||||||
|
&dir_path,
|
||||||
|
HashMap::from([(
|
||||||
|
DEFAULT_INDEX_NAME.to_string(),
|
||||||
|
Box::new(index.clone()) as Box<dyn Index>,
|
||||||
|
)]),
|
||||||
|
user_manifest,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let tree = project.manifest().dependency_tree(&project, false).unwrap();
|
let manifest = project.manifest().clone();
|
||||||
|
let tree = manifest.dependency_tree(&mut project, false).unwrap();
|
||||||
assert_eq!(tree.len(), 1);
|
assert_eq!(tree.len(), 1);
|
||||||
let versions = tree.get(&pkg_name).unwrap();
|
let versions = tree.get(&pkg_name.clone().into()).unwrap();
|
||||||
assert_eq!(versions.len(), 2);
|
assert_eq!(versions.len(), 2);
|
||||||
let resolved_pkg = versions.get(&version).unwrap();
|
let resolved_pkg = versions.get(&version).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -94,6 +109,7 @@ fn test_resolves_package() {
|
||||||
pkg_ref: PackageRef::Registry(RegistryPackageRef {
|
pkg_ref: PackageRef::Registry(RegistryPackageRef {
|
||||||
name: pkg_name.clone(),
|
name: pkg_name.clone(),
|
||||||
version: version.clone(),
|
version: version.clone(),
|
||||||
|
index_url: index.url().clone(),
|
||||||
}),
|
}),
|
||||||
specifier,
|
specifier,
|
||||||
dependencies: Default::default(),
|
dependencies: Default::default(),
|
||||||
|
@ -109,6 +125,7 @@ fn test_resolves_package() {
|
||||||
pkg_ref: PackageRef::Registry(RegistryPackageRef {
|
pkg_ref: PackageRef::Registry(RegistryPackageRef {
|
||||||
name: pkg_name.clone(),
|
name: pkg_name.clone(),
|
||||||
version: version_2.clone(),
|
version: version_2.clone(),
|
||||||
|
index_url: index.url().clone(),
|
||||||
}),
|
}),
|
||||||
specifier: specifier_2,
|
specifier: specifier_2,
|
||||||
dependencies: Default::default(),
|
dependencies: Default::default(),
|
||||||
|
|
Loading…
Reference in a new issue