diff --git a/.gitignore b/.gitignore index 62ef1b5d..6765ad1a 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ /*.rockspec *.snap.new /.rocks + +.pre-commit-config.yaml diff --git a/rocks-lib/src/build/variables.rs b/rocks-lib/src/build/variables.rs index cdaf5cd4..7e52dd65 100644 --- a/rocks-lib/src/build/variables.rs +++ b/rocks-lib/src/build/variables.rs @@ -45,9 +45,9 @@ mod tests { #[test] fn substitute_helper() { - assert_eq!(substitute(get_var, "$(TEST_VAR)".into()), "foo".to_string()); + assert_eq!(substitute(get_var, "$(TEST_VAR)"), "foo".to_string()); assert_eq!( - substitute(get_var, "$(UNRECOGNISED)".into()), + substitute(get_var, "$(UNRECOGNISED)"), "$(UNRECOGNISED)".to_string() ); } diff --git a/rocks-lib/src/lockfile/mod.rs b/rocks-lib/src/lockfile/mod.rs index d12f8b8b..0a865b60 100644 --- a/rocks-lib/src/lockfile/mod.rs +++ b/rocks-lib/src/lockfile/mod.rs @@ -1,5 +1,8 @@ +use std::error::Error; use std::fmt::Display; use std::io::{self, Write}; +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; use std::{collections::HashMap, fs::File, io::ErrorKind, path::PathBuf}; use itertools::Itertools; @@ -412,10 +415,21 @@ impl TryFrom<&Option> for LockConstraint { } } +pub trait LockfilePermissions {} +#[derive(Clone)] +pub struct ReadOnly; +#[derive(Clone)] +pub struct ReadWrite; + +impl LockfilePermissions for ReadOnly {} +impl LockfilePermissions for ReadWrite {} + #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Lockfile { +pub struct Lockfile { #[serde(skip)] filepath: PathBuf, + #[serde(skip)] + _marker: PhantomData

, // TODO: Serialize this directly into a `Version` version: String, // NOTE: We cannot directly serialize to a `Sha256` object as they don't implement serde traits. @@ -423,8 +437,63 @@ pub struct Lockfile { entrypoints: Vec, } -impl Lockfile { - pub fn new(filepath: PathBuf) -> io::Result { +impl Lockfile

{ + pub fn version(&self) -> &String { + &self.version + } + + pub fn rocks(&self) -> &HashMap { + &self.rocks + } + + pub fn get(&self, id: &LocalPackageId) -> Option<&LocalPackage> { + self.rocks.get(id) + } + + pub(crate) fn list(&self) -> HashMap> { + self.rocks() + .values() + .cloned() + .map(|locked_rock| (locked_rock.name().clone(), locked_rock)) + .into_group_map() + } + + pub(crate) fn has_rock(&self, req: &PackageReq) -> Option { + self.list() + .get(req.name()) + .map(|packages| { + packages + .iter() + .rev() + .find(|package| req.version_req().matches(package.version())) + })? + .cloned() + } + + fn flush(&mut self) -> io::Result<()> { + let dependencies = self + .rocks + .iter() + .flat_map(|(_, rock)| rock.dependencies()) + .collect_vec(); + + self.entrypoints = self + .rocks + .keys() + .filter(|id| !dependencies.iter().contains(&id)) + .cloned() + .collect(); + + let content = serde_json::to_string_pretty(&self)?; + + std::fs::write(&self.filepath, content)?; + + Ok(()) + } +} + +impl Lockfile { + pub(crate) fn new(filepath: PathBuf) -> io::Result> { // Ensure that the lockfile exists match File::options().create_new(true).write(true).open(&filepath) { Ok(mut file) => { @@ -443,13 +512,68 @@ impl Lockfile { Err(err) => return Err(err), } - let mut new: Lockfile = serde_json::from_str(&std::fs::read_to_string(&filepath)?)?; + let mut new: Lockfile = + serde_json::from_str(&std::fs::read_to_string(&filepath)?)?; new.filepath = filepath; Ok(new) } + /// Creates a temporary, writeable lockfile which can never flush. + pub fn into_temporary(self) -> Lockfile { + Lockfile:: { + _marker: PhantomData, + filepath: self.filepath, + version: self.version, + rocks: self.rocks, + entrypoints: self.entrypoints, + } + } + + /// Creates a lockfile guard, flushing the lockfile automatically + /// once the guard goes out of scope. + pub fn write_guard(self) -> LockfileGuard { + LockfileGuard(self.into_temporary()) + } + + /// Converts the current lockfile into a writeable one, executes `cb` and flushes + /// the lockfile. + pub fn map_then_flush(self, cb: F) -> Result + where + F: FnOnce(&mut Lockfile) -> Result, + E: Error, + E: From, + { + let mut writeable_lockfile = self.into_temporary(); + + let result = cb(&mut writeable_lockfile)?; + + writeable_lockfile.flush()?; + + Ok(result) + } + + // TODO: Add this once async closures are stabilized + // Converts the current lockfile into a writeable one, executes `cb` asynchronously and flushes + // the lockfile. + //pub async fn map_then_flush_async(self, cb: F) -> Result + //where + // F: AsyncFnOnce(&mut Lockfile) -> Result, + // E: Error, + // E: From, + //{ + // let mut writeable_lockfile = self.into_temporary(); + // + // let result = cb(&mut writeable_lockfile).await?; + // + // writeable_lockfile.flush()?; + // + // Ok(result) + //} +} + +impl Lockfile { pub fn add(&mut self, rock: &LocalPackage) { self.rocks.insert(rock.id(), rock.clone()); } @@ -477,74 +601,53 @@ impl Lockfile { self.rocks.remove(target); } - pub fn version(&self) -> &String { - &self.version - } + // TODO: `fn entrypoints() -> Vec` +} - pub fn rocks(&self) -> &HashMap { - &self.rocks - } +pub struct LockfileGuard(Lockfile); - pub fn get(&self, id: &LocalPackageId) -> Option<&LocalPackage> { - self.rocks.get(id) +impl Serialize for LockfileGuard { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.0.serialize(serializer) } +} - pub fn get_mut(&mut self, id: &LocalPackageId) -> Option<&mut LocalPackage> { - self.rocks.get_mut(id) +impl<'de> Deserialize<'de> for LockfileGuard { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + Ok(LockfileGuard(Lockfile::::deserialize( + deserializer, + )?)) } +} - // TODO: `fn entrypoints() -> Vec` - - pub fn flush(&mut self) -> io::Result<()> { - let dependencies = self - .rocks - .iter() - .flat_map(|(_, rock)| rock.dependencies()) - .collect_vec(); - - self.entrypoints = self - .rocks - .keys() - .filter(|id| !dependencies.iter().contains(&id)) - .cloned() - .collect(); - - let content = serde_json::to_string_pretty(self)?; - - std::fs::write(&self.filepath, content)?; +impl Deref for LockfileGuard { + type Target = Lockfile; - Ok(()) - } - - pub(crate) fn list(&self) -> HashMap> { - self.rocks() - .values() - .cloned() - .map(|locked_rock| (locked_rock.name().clone(), locked_rock)) - .into_group_map() + fn deref(&self) -> &Self::Target { + &self.0 } +} - pub(crate) fn has_rock(&self, req: &PackageReq) -> Option { - self.list() - .get(req.name()) - .map(|packages| { - packages - .iter() - .rev() - .find(|package| req.version_req().matches(package.version())) - })? - .cloned() +impl DerefMut for LockfileGuard { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 } } -impl Drop for Lockfile { +impl Drop for LockfileGuard { fn drop(&mut self) { let _ = self.flush(); } } #[cfg(feature = "lua")] -impl mlua::UserData for Lockfile { +impl mlua::UserData for Lockfile { fn add_methods>(methods: &mut M) { methods.add_method_mut("add", |_, this, package: LocalPackage| { this.add(&package); @@ -622,7 +725,7 @@ mod tests { }; let tree = Tree::new(temp.to_path_buf(), Lua51).unwrap(); - let mut lockfile = tree.lockfile().unwrap(); + let mut lockfile = tree.lockfile().unwrap().write_guard(); let test_package = PackageSpec::parse("test1".to_string(), "0.1.0".to_string()).unwrap(); let test_local_package = LocalPackage::from( diff --git a/rocks-lib/src/luarocks/luarocks_installation.rs b/rocks-lib/src/luarocks/luarocks_installation.rs index a5f3756d..eef0728b 100644 --- a/rocks-lib/src/luarocks/luarocks_installation.rs +++ b/rocks-lib/src/luarocks/luarocks_installation.rs @@ -99,9 +99,11 @@ impl LuaRocksInstallation { &self, progress: &Progress, ) -> Result<(), LuaRocksInstallError> { - let mut lockfile = self.tree.lockfile()?; + let mut lockfile = self.tree.lockfile()?.write_guard(); + let luarocks_req = PackageReq::new("luarocks".into(), Some(LUAROCKS_VERSION.into())).unwrap(); + if !self.tree.match_rocks(&luarocks_req)?.is_found() { let rockspec = Rockspec::new(LUAROCKS_ROCKSPEC).unwrap(); let pkg = Build::new(&rockspec, &self.config, progress) @@ -112,7 +114,7 @@ impl LuaRocksInstallation { .await?; lockfile.add(&pkg); } - lockfile.flush()?; + Ok(()) } @@ -123,7 +125,7 @@ impl LuaRocksInstallation { progress_arc: Arc>, ) -> Result<(), InstallBuildDependenciesError> { let progress = Arc::clone(&progress_arc); - let mut lockfile = self.tree.lockfile()?; + let mut lockfile = self.tree.lockfile()?.write_guard(); let package_db = RemotePackageDB::from_config(&self.config).await?; let build_dependencies = match rockspec.rockspec_format { Some(RockspecFormat::_1_0 | RockspecFormat::_2_0) => { @@ -204,7 +206,7 @@ impl LuaRocksInstallation { ); }); }); - lockfile.flush()?; + Ok(()) } diff --git a/rocks-lib/src/operations/install.rs b/rocks-lib/src/operations/install.rs index face5708..ee31e8e8 100644 --- a/rocks-lib/src/operations/install.rs +++ b/rocks-lib/src/operations/install.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, io, sync::Arc}; use crate::{ build::{Build, BuildBehaviour, BuildError}, config::{Config, LuaVersion, LuaVersionUnset}, - lockfile::{LocalPackage, LocalPackageId, LockConstraint, Lockfile, PinnedState}, + lockfile::{LocalPackage, LocalPackageId, LockConstraint, Lockfile, PinnedState, ReadWrite}, luarocks::{ install_binary_rock::{BinaryRockInstall, InstallBinaryRockError}, luarocks_installation::{ @@ -137,10 +137,10 @@ where { let lua_version = LuaVersion::from(config)?; let tree = Tree::new(config.tree().clone(), lua_version)?; - let mut lockfile = tree.lockfile()?; - let result = install_impl(packages, pin, package_db, config, &mut lockfile, progress).await; - lockfile.flush()?; - result + + let mut lockfile = tree.lockfile()?.write_guard(); + + install_impl(packages, pin, package_db, config, &mut lockfile, progress).await } async fn install_impl( @@ -148,7 +148,7 @@ async fn install_impl( pin: PinnedState, package_db: RemotePackageDB, config: &Config, - lockfile: &mut Lockfile, + lockfile: &mut Lockfile, progress_arc: Arc>, ) -> Result, InstallError> { let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); diff --git a/rocks-lib/src/operations/pin.rs b/rocks-lib/src/operations/pin.rs index 666dbd0c..fe9cbd01 100644 --- a/rocks-lib/src/operations/pin.rs +++ b/rocks-lib/src/operations/pin.rs @@ -37,7 +37,7 @@ pub fn set_pinned_state( tree: &Tree, pin: PinnedState, ) -> Result<(), PinError> { - let mut lockfile = tree.lockfile()?; + let lockfile = tree.lockfile()?; let mut package = lockfile .get(package_id) .ok_or_else(|| PinError::PackageNotFound(package_id.clone()))? @@ -71,9 +71,12 @@ pub fn set_pinned_state( fs_extra::move_items(&items, new_root, &CopyOptions::new())?; - lockfile.remove(&old_package); - lockfile.add(&package); - lockfile.flush()?; + lockfile.map_then_flush(|lockfile| { + lockfile.remove(&old_package); + lockfile.add(&package); + + Ok::<_, io::Error>(()) + })?; Ok(()) } diff --git a/rocks-lib/src/operations/remove.rs b/rocks-lib/src/operations/remove.rs index ef559299..52d792f3 100644 --- a/rocks-lib/src/operations/remove.rs +++ b/rocks-lib/src/operations/remove.rs @@ -76,7 +76,7 @@ async fn remove( tree: Tree, progress: &Progress, ) -> Result<(), RemoveError> { - let mut lockfile = tree.lockfile()?; + let lockfile = tree.lockfile()?; let packages = package_ids .iter() @@ -92,11 +92,13 @@ async fn remove( })) .await; - package_ids - .iter() - .for_each(|package| lockfile.remove_by_id(package)); + lockfile.map_then_flush(|lockfile| { + package_ids + .iter() + .for_each(|package| lockfile.remove_by_id(package)); - lockfile.flush()?; + Ok::<_, io::Error>(()) + })?; Ok(()) } diff --git a/rocks-lib/src/operations/resolve.rs b/rocks-lib/src/operations/resolve.rs index 542676f6..5781b702 100644 --- a/rocks-lib/src/operations/resolve.rs +++ b/rocks-lib/src/operations/resolve.rs @@ -9,7 +9,10 @@ use tokio::sync::mpsc::UnboundedSender; use crate::{ build::BuildBehaviour, config::Config, - lockfile::{LocalPackageId, LocalPackageSpec, LockConstraint, Lockfile, PinnedState}, + lockfile::{ + LocalPackageId, LocalPackageSpec, LockConstraint, Lockfile, LockfilePermissions, + PinnedState, + }, package::{PackageReq, PackageVersionReq}, progress::{MultiProgress, Progress}, remote_package_db::RemotePackageDB, @@ -25,15 +28,18 @@ pub(crate) struct PackageInstallSpec { } #[async_recursion] -pub(crate) async fn get_all_dependencies( +pub(crate) async fn get_all_dependencies

( tx: UnboundedSender, packages: Vec<(BuildBehaviour, PackageReq)>, pin: PinnedState, package_db: Arc, - lockfile: Arc, + lockfile: Arc>, config: &Config, progress: Arc>, -) -> Result, SearchAndDownloadError> { +) -> Result, SearchAndDownloadError> +where + P: LockfilePermissions + Send + Sync + 'static, +{ join_all( packages .into_iter() diff --git a/rocks-lib/src/tree/mod.rs b/rocks-lib/src/tree/mod.rs index 18ddfaeb..d135cc6a 100644 --- a/rocks-lib/src/tree/mod.rs +++ b/rocks-lib/src/tree/mod.rs @@ -4,7 +4,7 @@ use crate::{ variables::{self, HasVariables}, }, config::LuaVersion, - lockfile::{LocalPackage, LocalPackageId, Lockfile}, + lockfile::{LocalPackage, LocalPackageId, Lockfile, ReadOnly}, package::PackageReq, }; use std::{io, path::PathBuf}; @@ -208,7 +208,7 @@ impl Tree { Ok(rock_layout) } - pub fn lockfile(&self) -> io::Result { + pub fn lockfile(&self) -> io::Result> { Lockfile::new(self.root().join("lock.json")) } } @@ -248,7 +248,9 @@ impl mlua::UserData for Tree { methods.add_method("rock", |_, this, package: LocalPackage| { this.rock(&package).into_lua_err() }); - methods.add_method("lockfile", |_, this, ()| this.lockfile().into_lua_err()); + methods.add_method("lockfile", |_, this, ()| { + Ok(this.lockfile().into_lua_err()?.into_temporary()) + }); } }