diff --git a/flake.nix b/flake.nix index aea4f856..17a79c6a 100644 --- a/flake.nix +++ b/flake.nix @@ -6,7 +6,6 @@ rust-overlay = { url = "github:oxalica/rust-overlay"; inputs.nixpkgs.follows = "nixpkgs"; - inputs.flake-utils.follows = "flake-utils"; }; advisory-db = { url = "github:rustsec/advisory-db"; diff --git a/src/command.rs b/src/command.rs index cb57151a..564e65aa 100644 --- a/src/command.rs +++ b/src/command.rs @@ -232,6 +232,7 @@ pub async fn package( directory: impl AsRef, dry_run: bool, version: Option, + preserve_mtime: bool, ) -> miette::Result<()> { let mut manifest = Manifest::read().await?; let store = PackageStore::current().await?; @@ -248,7 +249,7 @@ pub async fn package( store.populate(pkg).await?; } - let package = store.release(&manifest).await?; + let package = store.release(&manifest, preserve_mtime).await?; if dry_run { return Ok(()); @@ -275,6 +276,7 @@ pub async fn publish( #[cfg(feature = "git")] allow_dirty: bool, dry_run: bool, version: Option, + preserve_mtime: bool, ) -> miette::Result<()> { #[cfg(feature = "git")] async fn git_statuses() -> miette::Result> { @@ -345,7 +347,7 @@ pub async fn publish( store.populate(pkg).await?; } - let package = store.release(&manifest).await?; + let package = store.release(&manifest, preserve_mtime).await?; if dry_run { tracing::warn!(":: aborting upload due to dry run"); @@ -356,7 +358,9 @@ pub async fn publish( } /// Installs dependencies -pub async fn install() -> miette::Result<()> { +/// +/// if [preserve_mtime] is true, local dependencies will keep their modification time +pub async fn install(preserve_mtime: bool) -> miette::Result<()> { let manifest = Manifest::read().await?; let lockfile = Lockfile::read_or_default().await?; let store = PackageStore::current().await?; @@ -371,10 +375,15 @@ pub async fn install() -> miette::Result<()> { tracing::info!(":: installed {}@{}", pkg.name, pkg.version); } - let dependency_graph = - DependencyGraph::from_manifest(&manifest, &lockfile, &credentials.into(), &cache) - .await - .wrap_err(miette!("dependency resolution failed"))?; + let dependency_graph = DependencyGraph::from_manifest( + &manifest, + &lockfile, + &credentials.into(), + &cache, + preserve_mtime, + ) + .await + .wrap_err(miette!("dependency resolution failed"))?; let mut locked = Vec::new(); diff --git a/src/main.rs b/src/main.rs index 53644651..8efcdbbd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -94,6 +94,10 @@ enum Command { /// Note: This overrides the version in the manifest. #[clap(long)] set_version: Option, + /// Indicate whether access time information is preserved when creating a package. + /// Default is 'true' + #[clap(long)] + preserve_mtime: Option, }, /// Packages and uploads this api to the registry @@ -115,10 +119,19 @@ enum Command { /// Note: This overrides the version in the manifest. #[clap(long)] set_version: Option, + /// Indicate whether access time information is preserved when creating a package. + /// Default is 'true' + #[clap(long)] + preserve_mtime: Option, }, /// Installs dependencies - Install, + Install { + /// Indicate whether access time information is preserved when installing a local. + /// Default is 'true' + #[clap(long)] + preserve_local_mtime: Option, + }, /// Uninstalls dependencies Uninstall, @@ -229,23 +242,31 @@ async fn main() -> miette::Result<()> { output_directory, dry_run, set_version, - } => command::package(output_directory, dry_run, set_version) - .await - .wrap_err(miette!( - "failed to export `{package}` into the buffrs package format" - )), + preserve_mtime, + } => command::package( + output_directory, + dry_run, + set_version, + preserve_mtime.unwrap_or(true), + ) + .await + .wrap_err(miette!( + "failed to export `{package}` into the buffrs package format" + )), Command::Publish { registry, repository, allow_dirty, dry_run, set_version, + preserve_mtime, } => command::publish( registry.to_owned(), repository.to_owned(), allow_dirty, dry_run, set_version, + preserve_mtime.unwrap_or(true), ) .await .wrap_err(miette!( @@ -255,7 +276,9 @@ async fn main() -> miette::Result<()> { "failed to lint protocol buffers in `{}`", PackageStore::PROTO_PATH )), - Command::Install => command::install() + Command::Install { + preserve_local_mtime, + } => command::install(preserve_local_mtime.unwrap_or(true)) .await .wrap_err(miette!("failed to install dependencies for `{package}`")), Command::Uninstall => command::uninstall() diff --git a/src/package/compressed.rs b/src/package/compressed.rs index e11ebf40..fc17932f 100644 --- a/src/package/compressed.rs +++ b/src/package/compressed.rs @@ -16,6 +16,7 @@ use std::{ collections::BTreeMap, io::{self, Cursor, Read, Write}, path::{Path, PathBuf}, + time::UNIX_EPOCH, }; use bytes::{Buf, Bytes}; @@ -32,6 +33,8 @@ use crate::{ ManagedFile, }; +use super::store::Entry; + /// An in memory representation of a `buffrs` package #[derive(Clone, Debug, PartialEq, Eq)] pub struct Package { @@ -46,7 +49,11 @@ impl Package { /// /// This intentionally uses a [`BTreeMap`] to ensure that the list of files is sorted /// lexicographically. This ensures a reproducible output. - pub fn create(mut manifest: Manifest, files: BTreeMap) -> miette::Result { + pub fn create( + mut manifest: Manifest, + files: BTreeMap, + preserve_mtime: bool, + ) -> miette::Result { if manifest.edition == Edition::Unknown { manifest = Manifest::new(manifest.package, manifest.dependencies); } @@ -88,8 +95,23 @@ impl Package { .into_diagnostic() .wrap_err(miette!("failed to add manifest to release"))?; - for (name, contents) in &files { + for (name, entry) in &files { let mut header = tar::Header::new_gnu(); + + let Entry { contents, metadata } = entry; + + if preserve_mtime { + let mtime = metadata + .as_ref() + .and_then(|metadata| metadata.modified().ok()) + .and_then(|modified| modified.duration_since(UNIX_EPOCH).ok()) + .map(|duration| duration.as_secs()); + + if let Some(mtime) = mtime { + header.set_mtime(mtime); + } + } + header.set_mode(0o444); header.set_size(contents.len() as u64); archive diff --git a/src/package/store.rs b/src/package/store.rs index 4560ec4c..a948cbd6 100644 --- a/src/package/store.rs +++ b/src/package/store.rs @@ -18,6 +18,7 @@ use std::{ path::{Path, PathBuf}, }; +use bytes::Bytes; use miette::{bail, ensure, miette, Context, IntoDiagnostic}; use tokio::fs; use walkdir::WalkDir; @@ -151,7 +152,11 @@ impl PackageStore { } /// Packages a release from the local file system state - pub async fn release(&self, manifest: &Manifest) -> miette::Result { + pub async fn release( + &self, + manifest: &Manifest, + preserve_mtime: bool, + ) -> miette::Result { for dependency in manifest.dependencies.iter() { let resolved = self.resolve(&dependency.package).await?; @@ -171,10 +176,17 @@ impl PackageStore { for entry in self.collect(&pkg_path, false).await { let path = entry.strip_prefix(&pkg_path).into_diagnostic()?; let contents = tokio::fs::read(&entry).await.unwrap(); - entries.insert(path.into(), contents.into()); + + entries.insert( + path.into(), + Entry { + contents: contents.into(), + metadata: tokio::fs::metadata(&entry).await.ok(), + }, + ); } - let package = Package::create(manifest.clone(), entries)?; + let package = Package::create(manifest.clone(), entries, preserve_mtime)?; tracing::info!(":: packaged {}@{}", package.name(), package.version()); @@ -260,6 +272,13 @@ impl PackageStore { } } +pub struct Entry { + /// Actual bytes of the file + pub contents: Bytes, + /// File metadata, like mtime, ... + pub metadata: Option, +} + #[test] fn can_get_proto_path() { assert_eq!( diff --git a/src/resolver.rs b/src/resolver.rs index 8db06b25..80ee10a4 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -102,6 +102,37 @@ struct DownloadError { version: VersionReq, } +struct ProcessDependency<'a> { + name: PackageName, + dependency: Dependency, + is_root: bool, + lockfile: &'a Lockfile, + credentials: &'a Arc, + cache: &'a Cache, + preserve_mtime: bool, +} + +struct ProcessLocalDependency<'a> { + name: PackageName, + dependency: LocalDependency, + #[allow(dead_code)] + is_root: bool, + lockfile: &'a Lockfile, + credentials: &'a Arc, + cache: &'a Cache, + preserve_mtime: bool, +} + +struct ProcessRemoteDependency<'a> { + name: PackageName, + dependency: RemoteDependency, + is_root: bool, + lockfile: &'a Lockfile, + credentials: &'a Arc, + cache: &'a Cache, + preserve_mtime: bool, +} + impl DependencyGraph { /// Recursively resolves dependencies from the manifest to build a dependency graph pub async fn from_manifest( @@ -109,6 +140,7 @@ impl DependencyGraph { lockfile: &Lockfile, credentials: &Arc, cache: &Cache, + preserve_mtime: bool, ) -> miette::Result { let name = manifest .package @@ -120,13 +152,16 @@ impl DependencyGraph { for dependency in &manifest.dependencies { Self::process_dependency( - name.clone(), - dependency.clone(), - true, - lockfile, - credentials, - cache, &mut entries, + ProcessDependency { + name: name.clone(), + dependency: dependency.clone(), + is_root: true, + lockfile, + credentials, + cache, + preserve_mtime, + }, ) .await?; } @@ -135,42 +170,52 @@ impl DependencyGraph { } async fn process_dependency( - name: PackageName, - dependency: Dependency, - is_root: bool, - lockfile: &Lockfile, - credentials: &Arc, - cache: &Cache, entries: &mut HashMap, + params: ProcessDependency<'_>, ) -> miette::Result<()> { + let ProcessDependency { + name, + dependency, + is_root, + lockfile, + credentials, + cache, + preserve_mtime, + } = params; match dependency.manifest { DependencyManifest::Remote(manifest) => { Self::process_remote_dependency( - name.clone(), - RemoteDependency { - package: dependency.package, - manifest, - }, - is_root, - lockfile, - credentials, - cache, entries, + ProcessRemoteDependency { + name: name.clone(), + dependency: RemoteDependency { + package: dependency.package, + manifest, + }, + is_root, + lockfile, + credentials, + cache, + preserve_mtime, + }, ) .await?; } DependencyManifest::Local(manifest) => { Self::process_local_dependency( - name.clone(), - LocalDependency { - package: dependency.package, - manifest, - }, - is_root, - lockfile, - credentials, - cache, entries, + ProcessLocalDependency { + name: name.clone(), + dependency: LocalDependency { + package: dependency.package, + manifest, + }, + is_root, + lockfile, + credentials, + cache, + preserve_mtime, + }, ) .await?; } @@ -180,15 +225,19 @@ impl DependencyGraph { } #[async_recursion] - async fn process_local_dependency( - name: PackageName, - dependency: LocalDependency, - _: bool, - lockfile: &Lockfile, - credentials: &Arc, - cache: &Cache, - entries: &mut HashMap, + async fn process_local_dependency<'a>( + entries: &'a mut HashMap, + params: ProcessLocalDependency<'a>, ) -> miette::Result<()> { + let ProcessLocalDependency { + name, + dependency, + is_root: _, + lockfile, + credentials, + cache, + preserve_mtime, + } = params; let manifest = Manifest::try_read_from(&dependency.manifest.path.join(MANIFEST_FILE)) .await? .ok_or_else(|| { @@ -201,7 +250,7 @@ impl DependencyGraph { })?; let store = PackageStore::open(&dependency.manifest.path).await?; - let package = store.release(&manifest).await?; + let package = store.release(&manifest, preserve_mtime).await?; let dependency_name = package.name().clone(); let sub_dependencies = package.manifest.dependencies.clone(); @@ -225,13 +274,16 @@ impl DependencyGraph { for sub_dependency in sub_dependencies { Self::process_dependency( - dependency_name.clone(), - sub_dependency, - false, - lockfile, - credentials, - cache, entries, + ProcessDependency { + name: dependency_name.clone(), + dependency: sub_dependency, + is_root: false, + lockfile, + credentials, + cache, + preserve_mtime, + }, ) .await?; } @@ -240,15 +292,19 @@ impl DependencyGraph { } #[async_recursion] - async fn process_remote_dependency( - name: PackageName, - dependency: RemoteDependency, - is_root: bool, - lockfile: &Lockfile, - credentials: &Arc, - cache: &Cache, - entries: &mut HashMap, + async fn process_remote_dependency<'a>( + entries: &'a mut HashMap, + params: ProcessRemoteDependency<'a>, ) -> miette::Result<()> { + let ProcessRemoteDependency { + name, + dependency, + is_root, + lockfile, + credentials, + cache, + preserve_mtime, + } = params; let version_req = dependency.manifest.version.clone(); if let Some(entry) = entries.get_mut(&dependency.package) { @@ -307,13 +363,16 @@ impl DependencyGraph { for sub_dependency in sub_dependencies { Self::process_dependency( - dependency_name.clone(), - sub_dependency, - false, - lockfile, - credentials, - cache, entries, + ProcessDependency { + name: dependency_name.clone(), + dependency: sub_dependency, + is_root: false, + lockfile, + credentials, + cache, + preserve_mtime, + }, ) .await?; }