1use std::{
16 env,
17 path::{Path, PathBuf},
18 str::FromStr,
19};
20
21use miette::{Context as _, IntoDiagnostic, bail, ensure, miette};
22use semver::{Version, VersionReq};
23use tokio::{
24 fs,
25 io::{AsyncBufReadExt, BufReader, stdin},
26};
27
28use crate::{
29 credentials::Credentials,
30 io::File,
31 manifest::{
32 MANIFEST_FILE, Manifest,
33 package::{Dependency, PackageManifest, PackagesManifest},
34 },
35 operations::install::{Install, InstallationContext, NetworkMode},
36 operations::publish::Publisher,
37 package::{PackageName, PackageStore, PackageType},
38 registry::{Artifactory, RegistryUri},
39};
40
41const INITIAL_VERSION: Version = Version::new(0, 1, 0);
42const BUFFRS_TESTSUITE_VAR: &str = "BUFFRS_TESTSUITE";
43
44pub async fn init(kind: Option<PackageType>, name: Option<PackageName>) -> miette::Result<()> {
46 if PackagesManifest::exists().await? {
47 bail!("a manifest file was found, project is already initialized");
48 }
49
50 fn curr_dir_name() -> miette::Result<PackageName> {
51 std::env::current_dir()
52 .into_diagnostic()?
53 .file_name()
54 .ok_or(miette!(
56 "unexpected error: current directory path terminates in .."
57 ))?
58 .to_str()
59 .ok_or_else(|| miette!("current directory path is not valid utf-8"))?
60 .parse()
61 }
62
63 let package = kind
64 .map(|kind| -> miette::Result<PackageManifest> {
65 let name = name.map(Result::Ok).unwrap_or_else(curr_dir_name)?;
66
67 Ok(PackageManifest {
68 kind,
69 name,
70 version: INITIAL_VERSION,
71 description: None,
72 })
73 })
74 .transpose()?;
75
76 let mut builder = PackagesManifest::builder();
77
78 if let Some(pkg) = package {
79 builder = builder.package(pkg);
80 }
81
82 let manifest = builder.dependencies(Default::default()).build();
83
84 manifest.save_to(Path::new(".")).await?;
85
86 PackageStore::open(std::env::current_dir().unwrap_or_else(|_| ".".into()))
87 .await
88 .wrap_err("failed to create buffrs `proto` directories")?;
89
90 Ok(())
91}
92
93pub async fn new(kind: Option<PackageType>, name: PackageName) -> miette::Result<()> {
95 let package_dir = PathBuf::from(name.to_string());
96 fs::create_dir(&package_dir)
98 .await
99 .into_diagnostic()
100 .wrap_err_with(|| format!("failed to create {} directory", package_dir.display()))?;
101
102 let package = kind
103 .map(|kind| -> miette::Result<PackageManifest> {
104 Ok(PackageManifest {
105 kind,
106 name,
107 version: INITIAL_VERSION,
108 description: None,
109 })
110 })
111 .transpose()?;
112
113 let mut builder = PackagesManifest::builder();
114 if let Some(pkg) = package {
115 builder = builder.package(pkg);
116 }
117
118 let manifest = builder.dependencies(Default::default()).build();
119
120 manifest.save_to(package_dir.as_path()).await?;
121
122 PackageStore::open(&package_dir)
123 .await
124 .wrap_err("failed to create buffrs `proto` directories")?;
125
126 Ok(())
127}
128
129struct DependencyLocator {
130 repository: String,
131 package: PackageName,
132 version: DependencyLocatorVersion,
133}
134
135enum DependencyLocatorVersion {
136 Version(VersionReq),
137 Latest,
138}
139
140impl FromStr for DependencyLocator {
141 type Err = miette::Report;
142
143 fn from_str(dependency: &str) -> miette::Result<Self> {
144 let lower_kebab = |c: char| (c.is_lowercase() && c.is_ascii_alphabetic()) || c == '-';
145
146 let (repository, dependency) = dependency
147 .trim()
148 .split_once('/')
149 .ok_or_else(|| miette!("locator {dependency} is missing a repository delimiter"))?;
150
151 ensure!(
152 repository.chars().all(lower_kebab),
153 "repository {repository} is not in kebab case"
154 );
155
156 ensure!(!repository.is_empty(), "repository must not be empty");
157
158 let repository = repository.into();
159
160 let (package, version) = dependency
161 .split_once('@')
162 .map(|(package, version)| (package, Some(version)))
163 .unwrap_or_else(|| (dependency, None));
164
165 let package = package
166 .parse::<PackageName>()
167 .wrap_err_with(|| format!("invalid package name: {package}"))?;
168
169 let version = match version {
170 Some("latest") | None => DependencyLocatorVersion::Latest,
171 Some(version_str) => {
172 let parsed_version = VersionReq::parse(version_str)
173 .into_diagnostic()
174 .wrap_err_with(|| format!("not a valid version requirement: {version_str}"))?;
175 DependencyLocatorVersion::Version(parsed_version)
176 }
177 };
178
179 Ok(Self {
180 repository,
181 package,
182 version,
183 })
184 }
185}
186
187pub async fn add(registry: RegistryUri, dependency: &str) -> miette::Result<()> {
189 let manifest_path = PathBuf::from(MANIFEST_FILE);
190 let mut manifest = Manifest::require_package_manifest(&manifest_path).await?;
191
192 let DependencyLocator {
193 repository,
194 package,
195 version,
196 } = dependency.parse()?;
197
198 let version = match version {
199 DependencyLocatorVersion::Version(version_req) => version_req,
200 DependencyLocatorVersion::Latest => {
201 let credentials = Credentials::load().await?;
203 let artifactory = Artifactory::new(registry.clone(), &credentials)?;
204
205 let latest_version = artifactory
206 .get_latest_version(repository.clone(), package.clone())
207 .await?;
208 VersionReq::parse(&latest_version.to_string()).into_diagnostic()?
210 }
211 };
212
213 manifest
214 .dependencies
215 .get_or_insert_default()
216 .push(Dependency::new(registry, repository, package, version));
217
218 manifest
219 .save_to(Path::new("."))
220 .await
221 .wrap_err_with(|| format!("failed to write `{MANIFEST_FILE}`"))?;
222
223 Ok(())
224}
225
226pub async fn remove(package: PackageName) -> miette::Result<()> {
228 let manifest_path = PathBuf::from(MANIFEST_FILE);
229 let mut manifest = Manifest::require_package_manifest(&manifest_path).await?;
230 let store = PackageStore::current().await?;
231
232 let dependency = manifest
233 .dependencies
234 .iter()
235 .flatten()
236 .position(|d| d.package == package)
237 .ok_or_else(|| miette!("package {package} not in manifest"))?;
238
239 let dependency = manifest
240 .dependencies
241 .get_or_insert_default()
242 .remove(dependency);
243
244 store.uninstall(&dependency.package).await.ok();
245
246 manifest.save_to(Path::new(".")).await
247}
248
249pub async fn package(
251 directory: impl AsRef<Path>,
252 dry_run: bool,
253 version: Option<Version>,
254 preserve_mtime: bool,
255) -> miette::Result<()> {
256 let manifest_path = PathBuf::from(MANIFEST_FILE);
257 let manifest = Manifest::require_package_manifest(&manifest_path)
258 .await?
259 .with_version(version);
260 let store = PackageStore::current().await?;
261
262 if let Some(ref pkg) = manifest.package {
263 store.populate(pkg).await?;
264 }
265
266 let package = store.release(&manifest, preserve_mtime).await?;
267
268 if dry_run {
269 return Ok(());
270 }
271
272 let path = {
273 let file = format!("{}-{}.tgz", package.name(), package.version());
274
275 directory.as_ref().join(file)
276 };
277
278 fs::write(path, package.tgz)
279 .await
280 .into_diagnostic()
281 .wrap_err("failed to write package release to the current directory")
282}
283
284pub async fn publish(
286 registry: RegistryUri,
287 repository: String,
288 #[cfg(feature = "git")] allow_dirty: bool,
289 dry_run: bool,
290 version: Option<Version>,
291 preserve_mtime: bool,
292) -> miette::Result<()> {
293 #[cfg(feature = "git")]
294 Publisher::check_git_status(allow_dirty).await?;
295
296 let manifest = Manifest::load().await?;
297 let current_path = env::current_dir()
298 .into_diagnostic()
299 .wrap_err("current dir could not be retrieved")?;
300
301 let mut publisher = Publisher::new(registry, repository, preserve_mtime).await?;
302 publisher
303 .publish(&manifest, ¤t_path, version, dry_run)
304 .await
305}
306
307pub async fn install(preserve_mtime: bool, network_mode: NetworkMode) -> miette::Result<()> {
318 let manifest = Manifest::load().await?;
319
320 let ctx = InstallationContext::cwd(preserve_mtime, network_mode).await?;
321
322 manifest.install(&ctx).await?;
323
324 Ok(())
325}
326
327pub async fn uninstall() -> miette::Result<()> {
333 let manifest = Manifest::load().await?;
334
335 match manifest {
336 Manifest::Package(_) => PackageStore::current().await?.clear().await,
337 Manifest::Workspace(workspace_manifest) => {
338 let root_path = env::current_dir()
339 .into_diagnostic()
340 .wrap_err("current dir could not be retrieved")?;
341
342 let packages = workspace_manifest.workspace.members(root_path)?;
343
344 tracing::info!(
345 "workspace found. uninstalling dependencies for {} packages in workspace",
346 packages.len()
347 );
348
349 for package_path in packages {
350 tracing::info!(
351 "uninstalling dependencies for package: {}",
352 package_path.display()
353 );
354
355 let store = PackageStore::open(&package_path).await?;
356 store.clear().await?;
357 }
358
359 Ok(())
360 }
361 }
362}
363
364pub async fn list() -> miette::Result<()> {
366 let manifest_path = PathBuf::from(MANIFEST_FILE);
367 let manifest = Manifest::require_package_manifest(&manifest_path).await?;
368 let store = PackageStore::current().await?;
369
370 if let Some(ref pkg) = manifest.package {
371 store.populate(pkg).await?;
372 }
373
374 let protos = store.collect(&store.proto_vendor_path(), true).await;
375
376 let cwd = {
377 let cwd = std::env::current_dir()
378 .into_diagnostic()
379 .wrap_err("failed to get current directory")?;
380
381 fs::canonicalize(cwd)
382 .await
383 .into_diagnostic()
384 .wrap_err("failed to canonicalize current directory")?
385 };
386
387 for proto in protos.iter() {
388 let rel = proto
389 .strip_prefix(&cwd)
390 .into_diagnostic()
391 .wrap_err("failed to transform protobuf path")?;
392
393 print!("{} ", rel.display())
394 }
395
396 Ok(())
397}
398
399#[cfg(feature = "validation")]
401pub async fn lint() -> miette::Result<()> {
402 let manifest_path = PathBuf::from(MANIFEST_FILE);
403 let manifest = Manifest::require_package_manifest(&manifest_path).await?;
404 let store = PackageStore::current().await?;
405
406 let pkg = manifest.package.ok_or(miette!(
407 "a [package] section must be declared run the linter"
408 ))?;
409
410 store.populate(&pkg).await?;
411 let violations = store.validate(&pkg).await?;
412
413 violations
414 .into_iter()
415 .map(miette::Report::new)
416 .for_each(|r| eprintln!("{r:?}"));
417
418 Ok(())
419}
420
421pub async fn login(registry: RegistryUri) -> miette::Result<()> {
423 let mut credentials = Credentials::load().await?;
424
425 tracing::info!("please enter your artifactory token:");
426
427 let token = {
428 let mut raw = String::new();
429 let mut reader = BufReader::new(stdin());
430
431 reader
432 .read_line(&mut raw)
433 .await
434 .into_diagnostic()
435 .wrap_err("failed to read the token from the user")?;
436
437 raw.trim().into()
438 };
439
440 credentials.registry_tokens.insert(registry.clone(), token);
441
442 if env::var(BUFFRS_TESTSUITE_VAR).is_err() {
443 Artifactory::new(registry, &credentials)?
444 .ping()
445 .await
446 .wrap_err("failed to validate token")?;
447 }
448
449 credentials.write().await
450}
451
452pub async fn logout(registry: RegistryUri) -> miette::Result<()> {
454 let mut credentials = Credentials::load().await?;
455 credentials.registry_tokens.remove(®istry);
456 credentials.write().await
457}
458
459pub mod lock {
461 use crate::io::File;
462 use crate::lock::{FileRequirement, Lockfile};
463
464 pub async fn print_files() -> miette::Result<()> {
466 let lock = Lockfile::load().await?;
467
468 let requirements: Vec<FileRequirement> = lock.into();
469
470 if let Ok(json) = serde_json::to_string_pretty(&requirements) {
472 println!("{json}");
473 }
474
475 Ok(())
476 }
477}
478
479#[cfg(test)]
480mod tests {
481 use super::DependencyLocator;
482
483 #[test]
484 fn valid_dependency_locator() {
485 assert!("repo/pkg@1.0.0".parse::<DependencyLocator>().is_ok());
486 assert!("repo/pkg@=1.0".parse::<DependencyLocator>().is_ok());
487 assert!(
488 "repo-with-dash/pkg@=1.0"
489 .parse::<DependencyLocator>()
490 .is_ok()
491 );
492 assert!(
493 "repo-with-dash/pkg-with-dash@=1.0"
494 .parse::<DependencyLocator>()
495 .is_ok()
496 );
497 assert!(
498 "repo/pkg@=1.0.0-with-prerelease"
499 .parse::<DependencyLocator>()
500 .is_ok()
501 );
502 assert!("repo/pkg@latest".parse::<DependencyLocator>().is_ok());
503 assert!("repo/pkg".parse::<DependencyLocator>().is_ok());
504 }
505
506 #[test]
507 fn invalid_dependency_locators() {
508 assert!("/xyz@1.0.0".parse::<DependencyLocator>().is_err());
509 assert!("repo/@1.0.0".parse::<DependencyLocator>().is_err());
510 assert!("repo@1.0.0".parse::<DependencyLocator>().is_err());
511 assert!(
512 "repo/pkg@latestwithtypo"
513 .parse::<DependencyLocator>()
514 .is_err()
515 );
516 assert!("repo/pkg@=1#meta".parse::<DependencyLocator>().is_err());
517 assert!("repo/PKG@=1.0".parse::<DependencyLocator>().is_err());
518 }
519}