use std::{
env,
path::{Path, PathBuf},
str::FromStr,
};
use miette::{Context as _, IntoDiagnostic, bail, ensure, miette};
use semver::{Version, VersionReq};
use tokio::{
fs,
io::{AsyncBufReadExt, BufReader, stdin},
};
use crate::{
credentials::Credentials,
io::File,
manifest::{
MANIFEST_FILE, Manifest,
package::{Dependency, PackageManifest, PackagesManifest},
},
operations::install::{Install, InstallationContext, NetworkMode},
operations::publish::Publisher,
package::{PackageName, PackageStore, PackageType},
registry::{Artifactory, RegistryUri},
};
const INITIAL_VERSION: Version = Version::new(0, 1, 0);
const BUFFRS_TESTSUITE_VAR: &str = "BUFFRS_TESTSUITE";
pub async fn init(kind: Option<PackageType>, name: Option<PackageName>) -> miette::Result<()> {
if PackagesManifest::exists().await? {
bail!("a manifest file was found, project is already initialized");
}
fn curr_dir_name() -> miette::Result<PackageName> {
std::env::current_dir()
.into_diagnostic()?
.file_name()
.ok_or(miette!(
"unexpected error: current directory path terminates in .."
))?
.to_str()
.ok_or_else(|| miette!("current directory path is not valid utf-8"))?
.parse()
}
let package = kind
.map(|kind| -> miette::Result<PackageManifest> {
let name = name.map(Result::Ok).unwrap_or_else(curr_dir_name)?;
Ok(PackageManifest {
kind,
name,
version: INITIAL_VERSION,
description: None,
})
})
.transpose()?;
let mut builder = PackagesManifest::builder();
if let Some(pkg) = package {
builder = builder.package(pkg);
}
let manifest = builder.dependencies(Default::default()).build();
manifest.save_to(Path::new(".")).await?;
PackageStore::open(std::env::current_dir().unwrap_or_else(|_| ".".into()))
.await
.wrap_err("failed to create buffrs `proto` directories")?;
Ok(())
}
pub async fn new(kind: Option<PackageType>, name: PackageName) -> miette::Result<()> {
let package_dir = PathBuf::from(name.to_string());
fs::create_dir(&package_dir)
.await
.into_diagnostic()
.wrap_err_with(|| format!("failed to create {} directory", package_dir.display()))?;
let package = kind
.map(|kind| -> miette::Result<PackageManifest> {
Ok(PackageManifest {
kind,
name,
version: INITIAL_VERSION,
description: None,
})
})
.transpose()?;
let mut builder = PackagesManifest::builder();
if let Some(pkg) = package {
builder = builder.package(pkg);
}
let manifest = builder.dependencies(Default::default()).build();
manifest.save_to(package_dir.as_path()).await?;
PackageStore::open(&package_dir)
.await
.wrap_err("failed to create buffrs `proto` directories")?;
Ok(())
}
struct DependencyLocator {
repository: String,
package: PackageName,
version: DependencyLocatorVersion,
}
enum DependencyLocatorVersion {
Version(VersionReq),
Latest,
}
impl FromStr for DependencyLocator {
type Err = miette::Report;
fn from_str(dependency: &str) -> miette::Result<Self> {
let lower_kebab = |c: char| (c.is_lowercase() && c.is_ascii_alphabetic()) || c == '-';
let (repository, dependency) = dependency
.trim()
.split_once('/')
.ok_or_else(|| miette!("locator {dependency} is missing a repository delimiter"))?;
ensure!(
repository.chars().all(lower_kebab),
"repository {repository} is not in kebab case"
);
ensure!(!repository.is_empty(), "repository must not be empty");
let repository = repository.into();
let (package, version) = dependency
.split_once('@')
.map(|(package, version)| (package, Some(version)))
.unwrap_or_else(|| (dependency, None));
let package = package
.parse::<PackageName>()
.wrap_err_with(|| format!("invalid package name: {package}"))?;
let version = match version {
Some("latest") | None => DependencyLocatorVersion::Latest,
Some(version_str) => {
let parsed_version = VersionReq::parse(version_str)
.into_diagnostic()
.wrap_err_with(|| format!("not a valid version requirement: {version_str}"))?;
DependencyLocatorVersion::Version(parsed_version)
}
};
Ok(Self {
repository,
package,
version,
})
}
}
pub async fn add(registry: RegistryUri, dependency: &str) -> miette::Result<()> {
let manifest_path = PathBuf::from(MANIFEST_FILE);
let mut manifest = Manifest::require_package_manifest(&manifest_path).await?;
let DependencyLocator {
repository,
package,
version,
} = dependency.parse()?;
let version = match version {
DependencyLocatorVersion::Version(version_req) => version_req,
DependencyLocatorVersion::Latest => {
let credentials = Credentials::load().await?;
let artifactory = Artifactory::new(registry.clone(), &credentials)?;
let latest_version = artifactory
.get_latest_version(repository.clone(), package.clone())
.await?;
VersionReq::parse(&latest_version.to_string()).into_diagnostic()?
}
};
manifest
.dependencies
.get_or_insert_default()
.push(Dependency::new(registry, repository, package, version));
manifest
.save_to(Path::new("."))
.await
.wrap_err_with(|| format!("failed to write `{MANIFEST_FILE}`"))?;
Ok(())
}
pub async fn remove(package: PackageName) -> miette::Result<()> {
let manifest_path = PathBuf::from(MANIFEST_FILE);
let mut manifest = Manifest::require_package_manifest(&manifest_path).await?;
let store = PackageStore::current().await?;
let dependency = manifest
.dependencies
.iter()
.flatten()
.position(|d| d.package == package)
.ok_or_else(|| miette!("package {package} not in manifest"))?;
let dependency = manifest
.dependencies
.get_or_insert_default()
.remove(dependency);
store.uninstall(&dependency.package).await.ok();
manifest.save_to(Path::new(".")).await
}
pub async fn package(
directory: impl AsRef<Path>,
dry_run: bool,
version: Option<Version>,
preserve_mtime: bool,
) -> miette::Result<()> {
let manifest_path = PathBuf::from(MANIFEST_FILE);
let manifest = Manifest::require_package_manifest(&manifest_path)
.await?
.with_version(version);
let store = PackageStore::current().await?;
if let Some(ref pkg) = manifest.package {
store.populate(pkg).await?;
}
let package = store.release(&manifest, preserve_mtime).await?;
if dry_run {
return Ok(());
}
let path = {
let file = format!("{}-{}.tgz", package.name(), package.version());
directory.as_ref().join(file)
};
fs::write(path, package.tgz)
.await
.into_diagnostic()
.wrap_err("failed to write package release to the current directory")
}
pub async fn publish(
registry: RegistryUri,
repository: String,
#[cfg(feature = "git")] allow_dirty: bool,
dry_run: bool,
version: Option<Version>,
preserve_mtime: bool,
) -> miette::Result<()> {
#[cfg(feature = "git")]
Publisher::check_git_status(allow_dirty).await?;
let manifest = Manifest::load().await?;
let current_path = env::current_dir()
.into_diagnostic()
.wrap_err("current dir could not be retrieved")?;
let mut publisher = Publisher::new(registry, repository, preserve_mtime).await?;
publisher
.publish(&manifest, ¤t_path, version, dry_run)
.await
}
pub async fn install(preserve_mtime: bool, network_mode: NetworkMode) -> miette::Result<()> {
let manifest = Manifest::load().await?;
let ctx = InstallationContext::cwd(preserve_mtime, network_mode).await?;
manifest.install(&ctx).await?;
Ok(())
}
pub async fn uninstall() -> miette::Result<()> {
let manifest = Manifest::load().await?;
match manifest {
Manifest::Package(_) => PackageStore::current().await?.clear().await,
Manifest::Workspace(workspace_manifest) => {
let root_path = env::current_dir()
.into_diagnostic()
.wrap_err("current dir could not be retrieved")?;
let packages = workspace_manifest.workspace.members(root_path)?;
tracing::info!(
"workspace found. uninstalling dependencies for {} packages in workspace",
packages.len()
);
for package_path in packages {
tracing::info!(
"uninstalling dependencies for package: {}",
package_path.display()
);
let store = PackageStore::open(&package_path).await?;
store.clear().await?;
}
Ok(())
}
}
}
pub async fn list() -> miette::Result<()> {
let manifest_path = PathBuf::from(MANIFEST_FILE);
let manifest = Manifest::require_package_manifest(&manifest_path).await?;
let store = PackageStore::current().await?;
if let Some(ref pkg) = manifest.package {
store.populate(pkg).await?;
}
let protos = store.collect(&store.proto_vendor_path(), true).await;
let cwd = {
let cwd = std::env::current_dir()
.into_diagnostic()
.wrap_err("failed to get current directory")?;
fs::canonicalize(cwd)
.await
.into_diagnostic()
.wrap_err("failed to canonicalize current directory")?
};
for proto in protos.iter() {
let rel = proto
.strip_prefix(&cwd)
.into_diagnostic()
.wrap_err("failed to transform protobuf path")?;
print!("{} ", rel.display())
}
Ok(())
}
#[cfg(feature = "validation")]
pub async fn lint() -> miette::Result<()> {
let manifest_path = PathBuf::from(MANIFEST_FILE);
let manifest = Manifest::require_package_manifest(&manifest_path).await?;
let store = PackageStore::current().await?;
let pkg = manifest.package.ok_or(miette!(
"a [package] section must be declared run the linter"
))?;
store.populate(&pkg).await?;
let violations = store.validate(&pkg).await?;
violations
.into_iter()
.map(miette::Report::new)
.for_each(|r| eprintln!("{r:?}"));
Ok(())
}
pub async fn login(registry: RegistryUri) -> miette::Result<()> {
let mut credentials = Credentials::load().await?;
tracing::info!("please enter your artifactory token:");
let token = {
let mut raw = String::new();
let mut reader = BufReader::new(stdin());
reader
.read_line(&mut raw)
.await
.into_diagnostic()
.wrap_err("failed to read the token from the user")?;
raw.trim().into()
};
credentials.registry_tokens.insert(registry.clone(), token);
if env::var(BUFFRS_TESTSUITE_VAR).is_err() {
Artifactory::new(registry, &credentials)?
.ping()
.await
.wrap_err("failed to validate token")?;
}
credentials.write().await
}
pub async fn logout(registry: RegistryUri) -> miette::Result<()> {
let mut credentials = Credentials::load().await?;
credentials.registry_tokens.remove(®istry);
credentials.write().await
}
pub mod lock {
use crate::io::File;
use crate::lock::{FileRequirement, Lockfile};
pub async fn print_files() -> miette::Result<()> {
let lock = Lockfile::load().await?;
let requirements: Vec<FileRequirement> = lock.into();
if let Ok(json) = serde_json::to_string_pretty(&requirements) {
println!("{json}");
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::DependencyLocator;
#[test]
fn valid_dependency_locator() {
assert!("repo/pkg@1.0.0".parse::<DependencyLocator>().is_ok());
assert!("repo/pkg@=1.0".parse::<DependencyLocator>().is_ok());
assert!(
"repo-with-dash/pkg@=1.0"
.parse::<DependencyLocator>()
.is_ok()
);
assert!(
"repo-with-dash/pkg-with-dash@=1.0"
.parse::<DependencyLocator>()
.is_ok()
);
assert!(
"repo/pkg@=1.0.0-with-prerelease"
.parse::<DependencyLocator>()
.is_ok()
);
assert!("repo/pkg@latest".parse::<DependencyLocator>().is_ok());
assert!("repo/pkg".parse::<DependencyLocator>().is_ok());
}
#[test]
fn invalid_dependency_locators() {
assert!("/xyz@1.0.0".parse::<DependencyLocator>().is_err());
assert!("repo/@1.0.0".parse::<DependencyLocator>().is_err());
assert!("repo@1.0.0".parse::<DependencyLocator>().is_err());
assert!(
"repo/pkg@latestwithtypo"
.parse::<DependencyLocator>()
.is_err()
);
assert!("repo/pkg@=1#meta".parse::<DependencyLocator>().is_err());
assert!("repo/PKG@=1.0".parse::<DependencyLocator>().is_err());
}
}