feat: multi-index + wally support

This commit is contained in:
daimond113 2024-03-24 14:31:11 +01:00
parent e021c5f408
commit 984dd2ed0f
No known key found for this signature in database
GPG key ID: 3A8ECE51328B513C
27 changed files with 2244 additions and 794 deletions

473
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -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]

View file

@ -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

View file

@ -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"

View file

@ -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(&registry.name).unwrap().is_none() { if index
.package(&registry.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()

View file

@ -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();

View file

@ -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 => {

View file

@ -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");

View file

@ -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![];

View file

@ -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",

View file

@ -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()?;

View file

@ -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));

View file

@ -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) => &registry.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())?;
} }

View file

@ -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(())
} }

View file

@ -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) => &registry.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) => &registry.version, PackageRef::Registry(registry) => &registry.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
}

View file

@ -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() {

View file

@ -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(&registry_dependency.index))
.package(&registry_dependency.name) } else {
get_index_by_url(project.indices(), &registry_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
View 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,
})
}
}

View file

@ -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(&registry.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
}
}

View file

@ -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"));

View file

@ -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,

View file

@ -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

View file

@ -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)
} }
} }

View file

@ -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}");

View file

@ -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)?;

View file

@ -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
}
} }

View file

@ -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(),