diff --git a/rocks-bin/src/add.rs b/rocks-bin/src/add.rs index dc52b0d1..b1d742a4 100644 --- a/rocks-bin/src/add.rs +++ b/rocks-bin/src/add.rs @@ -1,16 +1,15 @@ -use eyre::{OptionExt, Result}; +use eyre::{Context, OptionExt, Result}; use rocks_lib::{ config::Config, lockfile::PinnedState, - operations, + luarocks::luarocks_installation::LuaRocksInstallation, + operations::Sync, package::PackageReq, progress::{MultiProgress, Progress, ProgressBar}, project::Project, remote_package_db::RemotePackageDB, }; -use crate::utils::install::apply_build_behaviour; - #[derive(clap::Args)] pub struct Add { /// Package or list of packages to install. @@ -41,56 +40,66 @@ pub async fn add(data: Add, config: Config) -> Result<()> { let tree = project.tree(&config)?; let db = RemotePackageDB::from_config(&config, &Progress::Progress(ProgressBar::new())).await?; - let regular_packages = apply_build_behaviour(data.package_req, pin, data.force, &tree); - if !regular_packages.is_empty() { + let progress = MultiProgress::new_arc(); + + if !data.package_req.is_empty() { + // NOTE: We only update the lockfile if one exists. + // Otherwise, the next `rocks build` will install the packages. + if let Some(mut lockfile) = project.try_lockfile()? { + Sync::new(&tree, &mut lockfile, &config) + .progress(progress.clone()) + .packages(data.package_req.clone()) + .pin(pin) + .sync_dependencies() + .await + .wrap_err("syncing dependencies with the project lockfile failed.")?; + } + project .add( - rocks_lib::project::DependencyType::Regular( - regular_packages - .iter() - .map(|(_, req)| req.clone()) - .collect(), - ), + rocks_lib::project::DependencyType::Regular(data.package_req), &db, ) .await?; } - let build_packages = - apply_build_behaviour(data.build.unwrap_or_default(), pin, data.force, &tree); + let build_packages = data.build.unwrap_or_default(); if !build_packages.is_empty() { + if let Some(mut lockfile) = project.try_lockfile()? { + let luarocks = LuaRocksInstallation::new(&config)?; + Sync::new(luarocks.tree(), &mut lockfile, luarocks.config()) + .progress(progress.clone()) + .packages(build_packages.clone()) + .pin(pin) + .sync_build_dependencies() + .await + .wrap_err("syncing build dependencies with the project lockfile failed.")?; + } + project .add( - rocks_lib::project::DependencyType::Build( - build_packages.iter().map(|(_, req)| req.clone()).collect(), - ), + rocks_lib::project::DependencyType::Build(build_packages), &db, ) .await?; } - let test_packages = - apply_build_behaviour(data.test.unwrap_or_default(), pin, data.force, &tree); + let test_packages = data.test.unwrap_or_default(); if !test_packages.is_empty() { - project - .add( - rocks_lib::project::DependencyType::Test( - test_packages.iter().map(|(_, req)| req.clone()).collect(), - ), - &db, - ) - .await?; - } + if let Some(mut lockfile) = project.try_lockfile()? { + Sync::new(&tree, &mut lockfile, &config) + .progress(progress.clone()) + .packages(test_packages.clone()) + .pin(pin) + .sync_test_dependencies() + .await + .wrap_err("syncing test dependencies with the project lockfile failed.")?; - operations::Install::new(&tree, &config) - .packages(regular_packages) - .packages(build_packages) - .packages(test_packages) - .pin(pin) - .progress(MultiProgress::new_arc()) - .project(&project) - .install() - .await?; + project + .add(rocks_lib::project::DependencyType::Test(test_packages), &db) + .await?; + } + } Ok(()) } diff --git a/rocks-bin/src/build.rs b/rocks-bin/src/build.rs index 62b9f3b1..23ee1303 100644 --- a/rocks-bin/src/build.rs +++ b/rocks-bin/src/build.rs @@ -8,6 +8,7 @@ use rocks_lib::{ build::{self, BuildBehaviour}, config::Config, lockfile::PinnedState, + luarocks::luarocks_installation::LuaRocksInstallation, operations::{Install, Sync}, package::PackageName, progress::MultiProgress, @@ -21,9 +22,9 @@ pub struct Build { #[arg(long)] pin: bool, - /// Ignore the project's existing lockfile. + /// Ignore the project's lockfile and don't create one. #[arg(long)] - ignore_lockfile: bool, + no_lock: bool, } pub async fn build(data: Build, config: Config) -> Result<()> { @@ -35,12 +36,6 @@ pub async fn build(data: Build, config: Config) -> Result<()> { let tree = project.tree(&config)?; let rocks = project.new_local_rockspec()?; - let lockfile = match project.try_lockfile()? { - None => None, - Some(_) if data.ignore_lockfile => None, - Some(lockfile) => Some(lockfile), - }; - let dependencies = rocks .dependencies() .current_platform() @@ -49,37 +44,79 @@ pub async fn build(data: Build, config: Config) -> Result<()> { .cloned() .collect_vec(); - match lockfile { - Some(mut project_lockfile) => { - Sync::new(&tree, &mut project_lockfile, &config) - .progress(progress.clone()) - .packages(dependencies) - .sync() - .await - .wrap_err( - " -syncing with the project lockfile failed. -Use --ignore-lockfile to force a new build. -", - )?; - } - None => { - let dependencies_to_install = dependencies - .into_iter() - .filter(|req| { - tree.match_rocks(req) - .is_ok_and(|rock_match| !rock_match.is_found()) - }) - .map(|dep| (BuildBehaviour::NoForce, dep)); - - Install::new(&tree, &config) - .packages(dependencies_to_install) + let build_dependencies = rocks + .build_dependencies() + .current_platform() + .iter() + .filter(|package| !package.name().eq(&PackageName::new("lua".into()))) + .cloned() + .collect_vec(); + + let luarocks = LuaRocksInstallation::new(&config)?; + + if data.no_lock { + let dependencies_to_install = dependencies + .into_iter() + .filter(|req| { + tree.match_rocks(req) + .is_ok_and(|rock_match| !rock_match.is_found()) + }) + .map(|dep| (BuildBehaviour::NoForce, dep)); + + Install::new(&tree, &config) + .packages(dependencies_to_install) + .project(&project) + .pin(pin) + .progress(progress.clone()) + .install() + .await?; + + let build_dependencies_to_install = build_dependencies + .into_iter() + .filter(|req| { + tree.match_rocks(req) + .is_ok_and(|rock_match| !rock_match.is_found()) + }) + .map(|dep| (BuildBehaviour::NoForce, dep)) + .collect_vec(); + + if !build_dependencies_to_install.is_empty() { + let bar = progress.map(|p| p.new_bar()); + luarocks.ensure_installed(&bar).await?; + Install::new(luarocks.tree(), luarocks.config()) + .packages(build_dependencies_to_install) .project(&project) .pin(pin) .progress(progress.clone()) .install() .await?; } + } else { + let mut project_lockfile = project.lockfile()?; + + Sync::new(&tree, &mut project_lockfile, &config) + .progress(progress.clone()) + .packages(dependencies) + .sync_dependencies() + .await + .wrap_err( + " +syncing dependencies with the project lockfile failed. +Use --ignore-lockfile to force a new build. +", + )?; + + Sync::new(luarocks.tree(), &mut project_lockfile, luarocks.config()) + .progress(progress.clone()) + .packages(build_dependencies) + .sync_build_dependencies() + .await + .wrap_err( + " +syncing build dependencies with the project lockfile failed. +Use --ignore-lockfile to force a new build. +", + )?; } build::Build::new(&rocks, &tree, &config, &progress.map(|p| p.new_bar())) diff --git a/rocks-bin/src/sync.rs b/rocks-bin/src/sync.rs index be206469..0aa0f37a 100644 --- a/rocks-bin/src/sync.rs +++ b/rocks-bin/src/sync.rs @@ -4,7 +4,7 @@ use clap::Args; use eyre::{eyre, Context, Result}; use rocks_lib::{ config::{Config, LuaVersion}, - lockfile::Lockfile, + lockfile::ProjectLockfile, operations, package::PackageReq, project::{rocks_toml::RocksToml, ROCKS_TOML}, @@ -34,7 +34,7 @@ pub struct Sync { pub async fn sync(args: Sync, config: Config) -> Result<()> { let tree = config.tree(LuaVersion::from(&config)?)?; - let mut lockfile = Lockfile::new(args.lockfile.clone())?; + let mut lockfile = ProjectLockfile::new(args.lockfile.clone())?; let mut sync = operations::Sync::new(&tree, &mut lockfile, &config) .validate_integrity(!args.no_integrity_check); @@ -71,7 +71,7 @@ pub async fn sync(args: Sync, config: Config) -> Result<()> { sync.add_packages(dependencies); } - sync.sync().await.wrap_err("sync failed.")?; + sync.sync_dependencies().await.wrap_err("sync failed.")?; Ok(()) } diff --git a/rocks-bin/src/test.rs b/rocks-bin/src/test.rs index 2792fc44..7f06910f 100644 --- a/rocks-bin/src/test.rs +++ b/rocks-bin/src/test.rs @@ -13,6 +13,10 @@ pub struct Test { /// Don't isolate the user environment (keep `HOME` and `XDG` environment variables). #[arg(long)] impure: bool, + + /// Ignore the project's lockfile and don't create one. + #[arg(long)] + no_lock: bool, } pub async fn test(test: Test, config: Config) -> Result<()> { @@ -27,6 +31,7 @@ pub async fn test(test: Test, config: Config) -> Result<()> { operations::Test::new(project, &config) .args(test_args) .env(test_env) + .no_lock(test.no_lock) .run() .await?; Ok(()) diff --git a/rocks-lib/resources/test/rocks.lock b/rocks-lib/resources/test/rocks.lock new file mode 100644 index 00000000..8751c1ee --- /dev/null +++ b/rocks-lib/resources/test/rocks.lock @@ -0,0 +1,207 @@ +{ + "version": "1.0.0", + "dependencies": { + "rocks": { + "028a2b47c550e0e17bd2952c7444b6bf64687b3cd6b59573a066a21f545466b8": { + "name": "lua-cjson", + "version": "2.1.0-1", + "pinned": false, + "dependencies": [], + "constraint": "=2.1.0", + "binaries": [ + "lua2json", + "json2lua" + ], + "source": "luarocks_rockspec+https://luarocks.org/", + "hashes": { + "rockspec": "sha256-L/6Yon2ek6NnEeifAy3qsnT2cMCB8IdaCgVeu3+1lvs=", + "source": "sha256-23r4ScVV0aR09yn+Sla1Uw6b57JHSet6fEdKfHIHuXI=" + } + }, + "04bfd5b08a74ba37c05241ef51d7afc47e1f3379c872dca34d99031b1ea7b855": { + "name": "neorg", + "version": "8.0.0-1", + "pinned": false, + "dependencies": [ + "99930d308895ccd027772f32eb777cebdb8668c32f4602036e237f0a46fcc0d9", + "985b1994517e6433d3e9ed05882a13069e7458f4646eabb451cc3964674c3c11" + ], + "constraint": "=8.0.0", + "binaries": [], + "source": "luarocks_rockspec+https://luarocks.org/", + "hashes": { + "rockspec": "sha256-7FQ1jtFfZQQ9IYtLwS14TcFsTWXM4xMnnpQh80GHpP0=", + "source": "sha256-YRQaq7LRoIK6qrc1djC/z4aPVhdcuw1IKxsZuddJKu4=" + } + }, + "085d597b2652e6dd9793a12fc57fca6d63575201113ba0bfe197f941264eb83f": { + "name": "say", + "version": "1.4.1-3", + "pinned": false, + "dependencies": [], + "constraint": ">=1.4.0", + "binaries": [], + "source": "luarocks_rockspec+https://luarocks.org/", + "hashes": { + "rockspec": "sha256-WFKt1iWeyjO9A8SG0KUX8tkS9JvMqoVM8CKBUguuK0Y=", + "source": "sha256-IjNkK1leVtYgbEjUqguVMjbdW+0BHAOCE0pazrVuF50=" + } + }, + "306b1bc37b09349a902e436efffb1161bbbdb46fafd5dc54b6e071ef5259b68a": { + "name": "nvim-nio", + "version": "1.10.1-1", + "pinned": false, + "dependencies": [], + "constraint": ">=1.8.0", + "binaries": [], + "source": "luarocks_rockspec+https://luarocks.org/", + "hashes": { + "rockspec": "sha256-QL5OCZFBGixecdEoriGck4iG83tjM09ewYbWVSbcfa4=", + "source": "sha256-5x+iHduNdyVjS6+OrqfDh17g9o2tP2YFFqKEsK6Z5zw=" + } + }, + "735d981050a43b2a36eb5621968fee7e6962ac1f673c9244a18039bf28594316": { + "name": "luassert", + "version": "1.9.0-1", + "pinned": false, + "dependencies": [ + "085d597b2652e6dd9793a12fc57fca6d63575201113ba0bfe197f941264eb83f" + ], + "constraint": null, + "binaries": [], + "source": "luarocks_rockspec+https://luarocks.org/", + "hashes": { + "rockspec": "sha256-rTPvF/GK/jMnH/q4wbwTCGBFELWh+JcvHeOCFAbIf64=", + "source": "sha256-jjdB95Vr5iVsh5T7E84WwZMW6/5H2k2R/ny2VBs2l3I=" + } + }, + "77a8e249ed3119392da5a9f20c88954cacf7ac6d2ccb7b78e037a218c6afc166": { + "name": "nvim-nio", + "version": "1.7.0-1", + "pinned": false, + "dependencies": [], + "constraint": ">=1.7.0, <1.8.0", + "binaries": [], + "source": "luarocks_rockspec+https://luarocks.org/", + "hashes": { + "rockspec": "sha256-BeisoicovxazR188FBSEKxr5FBrxpIL0Ss+vCdCQ1Aw=", + "source": "sha256-xuO1/iMXJnyKI8DkQO80TCTKMYmqOmc/bhlb9pDx6dY=" + } + }, + "985b1994517e6433d3e9ed05882a13069e7458f4646eabb451cc3964674c3c11": { + "name": "lua-utils.nvim", + "version": "1.0.2-1", + "pinned": false, + "dependencies": [], + "constraint": null, + "binaries": [], + "source": "luarocks_rockspec+https://luarocks.org/", + "hashes": { + "rockspec": "sha256-Kc8mdjLRaL063VWYjGAWSYySYKmFgZXF+Qoa0TaRIWg=", + "source": "sha256-fpGplQuldzNCYUMISOfGKgD080JKDxbZNb8Rch00Yxs=" + } + }, + "99930d308895ccd027772f32eb777cebdb8668c32f4602036e237f0a46fcc0d9": { + "name": "nvim-nio", + "version": "1.10.1-1", + "pinned": false, + "dependencies": [], + "constraint": null, + "binaries": [], + "source": "luarocks_rockspec+https://luarocks.org/", + "hashes": { + "rockspec": "sha256-QL5OCZFBGixecdEoriGck4iG83tjM09ewYbWVSbcfa4=", + "source": "sha256-5x+iHduNdyVjS6+OrqfDh17g9o2tP2YFFqKEsK6Z5zw=" + } + }, + "aa8d78a5981bc7a1a0ab12216849259a68e41c4203d495051ab57c24074f63f8": { + "name": "pathlib.nvim", + "version": "2.2.3-1", + "pinned": false, + "dependencies": [ + "306b1bc37b09349a902e436efffb1161bbbdb46fafd5dc54b6e071ef5259b68a" + ], + "constraint": ">=2.2.0, <2.3.0", + "binaries": [], + "source": "luarocks_rockspec+https://luarocks.org/", + "hashes": { + "rockspec": "sha256-kdDMqznWlwP9wIqlzPrZ5qEDp6edhlkaasAcQzWTmmM=", + "source": "sha256-fNO24tL8wApI8j3rk2mdLf5wbbjlUzsvCxki3n0xRw8=" + } + }, + "b171325ee5f129f7561a01d258ba38164d141e7cfddbf3c740edb28dd848ca2f": { + "name": "nui.nvim", + "version": "0.3.0-1", + "pinned": false, + "dependencies": [], + "constraint": "=0.3.0", + "binaries": [], + "source": "luarocks_rockspec+https://luarocks.org/", + "hashes": { + "rockspec": "sha256-C33kdZVqHui1mOd+J2/HbvmvYOntTMzfJ3YBtj4v51k=", + "source": "sha256-L0ebXtv794357HOAgT17xlEJsmpqIHGqGlYfDB20WTo=" + } + }, + "b49da3f0e8cc9fd87681b92011f04fbe1d9acedcdb1bae652040e5c16d33be29": { + "name": "plenary.nvim", + "version": "0.1.4-1", + "pinned": false, + "dependencies": [ + "735d981050a43b2a36eb5621968fee7e6962ac1f673c9244a18039bf28594316" + ], + "constraint": "=0.1.4", + "binaries": [], + "source": "luarocks_rockspec+https://luarocks.org/", + "hashes": { + "rockspec": "sha256-0EdepFBCsLc5D7HJKX66yMw/NbIgCmQib/+yqpJ9FFE=", + "source": "sha256-cJ2EfkbTOiEJ8IPVnoO77alXynBv+nJ2ql/vfYOTSJI=" + } + }, + "bf813f9f333f9efc8108b86386ea8a2a7aeaff1d5cdf470449d5e5242e3794d3": { + "name": "neorg", + "version": "8.8.1-1", + "pinned": false, + "dependencies": [ + "77a8e249ed3119392da5a9f20c88954cacf7ac6d2ccb7b78e037a218c6afc166", + "fb56666f3cfc66910e00c89bcfa81361660a0a4edfd528baadbedcd4ac89390a", + "b49da3f0e8cc9fd87681b92011f04fbe1d9acedcdb1bae652040e5c16d33be29", + "b171325ee5f129f7561a01d258ba38164d141e7cfddbf3c740edb28dd848ca2f", + "aa8d78a5981bc7a1a0ab12216849259a68e41c4203d495051ab57c24074f63f8" + ], + "constraint": "=8.8.1", + "binaries": [], + "source": "luarocks_rockspec+https://luarocks.org/", + "hashes": { + "rockspec": "sha256-fjnKNY8OIDpq2eZYea3v/uM4vna4OxpO+IA0LCcN+Cc=", + "source": "sha256-0sVLXN+aDxmb2lxvhjUGgg8XFyu8DMl2i2RA+TjUbu8=" + } + }, + "fb56666f3cfc66910e00c89bcfa81361660a0a4edfd528baadbedcd4ac89390a": { + "name": "lua-utils.nvim", + "version": "1.0.2-1", + "pinned": false, + "dependencies": [], + "constraint": "=1.0.2", + "binaries": [], + "source": "luarocks_rockspec+https://luarocks.org/", + "hashes": { + "rockspec": "sha256-Kc8mdjLRaL063VWYjGAWSYySYKmFgZXF+Qoa0TaRIWg=", + "source": "sha256-fpGplQuldzNCYUMISOfGKgD080JKDxbZNb8Rch00Yxs=" + } + } + }, + "entrypoints": [ + "028a2b47c550e0e17bd2952c7444b6bf64687b3cd6b59573a066a21f545466b8", + "04bfd5b08a74ba37c05241ef51d7afc47e1f3379c872dca34d99031b1ea7b855", + "bf813f9f333f9efc8108b86386ea8a2a7aeaff1d5cdf470449d5e5242e3794d3" + ] + }, + "test_dependencies": { + "rocks": {}, + "entrypoints": [] + }, + "build_dependencies": { + "rocks": {}, + "entrypoints": [] + } +} \ No newline at end of file diff --git a/rocks-lib/resources/test/sample-project-busted/.gitignore b/rocks-lib/resources/test/sample-project-busted/.gitignore index cf8e8c30..c294bf2a 100644 --- a/rocks-lib/resources/test/sample-project-busted/.gitignore +++ b/rocks-lib/resources/test/sample-project-busted/.gitignore @@ -1 +1,2 @@ .rocks +rocks.lock diff --git a/rocks-lib/src/lockfile/mod.rs b/rocks-lib/src/lockfile/mod.rs index 71c7daf8..a5aee569 100644 --- a/rocks-lib/src/lockfile/mod.rs +++ b/rocks-lib/src/lockfile/mod.rs @@ -468,14 +468,128 @@ pub struct ReadWrite; impl LockfilePermissions for ReadOnly {} impl LockfilePermissions for ReadWrite {} -#[derive(Clone, Debug, Serialize, Deserialize)] -struct LocalPackageLock { +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub(crate) struct LocalPackageLock { // NOTE: We cannot directly serialize to a `Sha256` object as they don't implement serde traits. // NOTE: We want to retain ordering of rocks and entrypoints when de/serializing. rocks: BTreeMap, entrypoints: Vec, } +impl LocalPackageLock { + fn get(&self, id: &LocalPackageId) -> Option<&LocalPackage> { + self.rocks.get(id) + } + + pub(crate) fn rocks(&self) -> &BTreeMap { + &self.rocks + } + + fn list(&self) -> HashMap> { + self.rocks() + .values() + .cloned() + .map(|locked_rock| (locked_rock.name().clone(), locked_rock)) + .into_group_map() + } + + fn remove(&mut self, target: &LocalPackage) { + self.remove_by_id(&target.id()) + } + + fn remove_by_id(&mut self, target: &LocalPackageId) { + self.rocks.remove(target); + self.entrypoints.retain(|x| x != target); + } + + pub(crate) fn has_rock( + &self, + req: &PackageReq, + filter: Option, + ) -> Option { + self.list() + .get(req.name()) + .map(|packages| { + packages + .iter() + .filter(|package| match &filter { + Some(filter_spec) => match package.source { + RemotePackageSource::LuarocksRockspec(_) => filter_spec.rockspec, + RemotePackageSource::LuarocksSrcRock(_) => filter_spec.src, + RemotePackageSource::LuarocksBinaryRock(_) => filter_spec.binary, + RemotePackageSource::RockspecContent(_) => true, + #[cfg(test)] + RemotePackageSource::Test => unimplemented!(), + }, + None => true, + }) + .rev() + .find(|package| req.version_req().matches(package.version())) + })? + .cloned() + } + + /// Synchronise a list of packages with this lockfile, + /// producing a report of packages to add and packages to remove. + /// + /// NOTE: The reason we produce a report and don't add/remove packages + /// here is because packages need to be installed in order to be added. + pub(crate) fn package_sync_spec(&self, packages: &[PackageReq]) -> PackageSyncSpec { + let (entrypoints_to_keep, entrypoints_to_remove): ( + HashSet, + HashSet, + ) = self + .entrypoints + .iter() + .map(|id| { + self.get(id) + .expect("entrypoint not found in malformed lockfile.") + }) + .cloned() + .partition(|local_pkg| { + packages + .iter() + .any(|req| req.matches(&local_pkg.as_package_spec())) + }); + + let packages_to_keep: HashSet<&LocalPackage> = entrypoints_to_keep + .iter() + .flat_map(|local_pkg| self.get_all_dependencies(&local_pkg.id())) + .collect(); + + let to_add = packages + .iter() + .filter(|pkg| self.has_rock(pkg, None).is_none()) + .cloned() + .collect_vec(); + + let to_remove = entrypoints_to_remove + .iter() + .flat_map(|local_pkg| self.get_all_dependencies(&local_pkg.id())) + .filter(|dependency| !packages_to_keep.contains(dependency)) + .cloned() + .collect_vec(); + + PackageSyncSpec { to_add, to_remove } + } + + /// Return all dependencies of a package, including itself + fn get_all_dependencies(&self, id: &LocalPackageId) -> HashSet<&LocalPackage> { + let mut packages = HashSet::new(); + if let Some(local_pkg) = self.get(id) { + packages.insert(local_pkg); + packages.extend( + local_pkg + .dependencies() + .iter() + .flat_map(|id| self.get_all_dependencies(id)), + ); + } + packages + } +} + +/// A lockfile for an install tree #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Lockfile { #[serde(skip)] @@ -488,6 +602,28 @@ pub struct Lockfile { lock: LocalPackageLock, } +pub(crate) enum LocalPackageLockType { + Regular, + Test, + Build, +} + +/// A lockfile for a Lua project +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ProjectLockfile { + #[serde(skip)] + filepath: PathBuf, + #[serde(skip)] + _marker: PhantomData

, + version: String, + #[serde(default)] + dependencies: LocalPackageLock, + #[serde(default)] + test_dependencies: LocalPackageLock, + #[serde(default)] + build_dependencies: LocalPackageLock, +} + #[derive(Error, Debug)] pub enum LockfileIntegrityError { #[error("rockspec integirty mismatch.\nExpected: {expected}\nBut got: {got}")] @@ -511,19 +647,19 @@ impl Lockfile

{ } pub fn rocks(&self) -> &BTreeMap { - &self.lock.rocks + self.lock.rocks() + } + + pub(crate) fn local_pkg_lock(&self) -> &LocalPackageLock { + &self.lock } pub fn get(&self, id: &LocalPackageId) -> Option<&LocalPackage> { - self.lock.rocks.get(id) + self.lock.get(id) } pub(crate) fn list(&self) -> HashMap> { - self.rocks() - .values() - .cloned() - .map(|locked_rock| (locked_rock.name().clone(), locked_rock)) - .into_group_map() + self.lock.list() } pub(crate) fn has_rock( @@ -531,26 +667,7 @@ impl Lockfile

{ req: &PackageReq, filter: Option, ) -> Option { - self.list() - .get(req.name()) - .map(|packages| { - packages - .iter() - .filter(|package| match &filter { - Some(filter_spec) => match package.source { - RemotePackageSource::LuarocksRockspec(_) => filter_spec.rockspec, - RemotePackageSource::LuarocksSrcRock(_) => filter_spec.src, - RemotePackageSource::LuarocksBinaryRock(_) => filter_spec.binary, - RemotePackageSource::RockspecContent(_) => true, - #[cfg(test)] - RemotePackageSource::Test => unimplemented!(), - }, - None => true, - }) - .rev() - .find(|package| req.version_req().matches(package.version())) - })? - .cloned() + self.lock.has_rock(req, filter) } /// Find all rocks that match the requirement @@ -633,6 +750,92 @@ impl Lockfile

{ } } +impl ProjectLockfile

{ + pub(crate) fn rocks( + &self, + deps: &LocalPackageLockType, + ) -> &BTreeMap { + match deps { + LocalPackageLockType::Regular => self.dependencies.rocks(), + LocalPackageLockType::Test => self.test_dependencies.rocks(), + LocalPackageLockType::Build => self.build_dependencies.rocks(), + } + } + + pub(crate) fn get( + &self, + id: &LocalPackageId, + deps: &LocalPackageLockType, + ) -> Option<&LocalPackage> { + match deps { + LocalPackageLockType::Regular => self.dependencies.get(id), + LocalPackageLockType::Test => self.test_dependencies.get(id), + LocalPackageLockType::Build => self.build_dependencies.get(id), + } + } + + pub(crate) fn local_pkg_lock(&self, deps: &LocalPackageLockType) -> &LocalPackageLock { + match deps { + LocalPackageLockType::Regular => &self.dependencies, + LocalPackageLockType::Test => &self.test_dependencies, + LocalPackageLockType::Build => &self.build_dependencies, + } + } + + fn flush(&mut self) -> io::Result<()> { + let dependencies = self + .dependencies + .rocks + .iter() + .flat_map(|(_, rock)| rock.dependencies()) + .collect_vec(); + + self.dependencies.entrypoints = self + .dependencies + .rocks + .keys() + .filter(|id| !dependencies.iter().contains(&id)) + .cloned() + .collect(); + + let test_dependencies = self + .test_dependencies + .rocks + .iter() + .flat_map(|(_, rock)| rock.dependencies()) + .collect_vec(); + + self.test_dependencies.entrypoints = self + .test_dependencies + .rocks + .keys() + .filter(|id| !test_dependencies.iter().contains(&id)) + .cloned() + .collect(); + + let build_dependencies = self + .build_dependencies + .rocks + .iter() + .flat_map(|(_, rock)| rock.dependencies()) + .collect_vec(); + + self.build_dependencies.entrypoints = self + .build_dependencies + .rocks + .keys() + .filter(|id| !build_dependencies.iter().contains(&id)) + .cloned() + .collect(); + + let content = serde_json::to_string_pretty(&self)?; + + std::fs::write(&self.filepath, content)?; + + Ok(()) + } +} + impl Lockfile { pub fn new(filepath: PathBuf) -> io::Result> { // Ensure that the lockfile exists @@ -661,66 +864,6 @@ impl Lockfile { Ok(new) } - /// Synchronise a list of packages with this lockfile, - /// producing a report of packages to add and packages to remove. - /// - /// NOTE: The reason we produce a report and don't add/remove packages - /// here is because packages need to be installed in order to be added. - pub(crate) fn package_sync_spec(&self, packages: &[PackageReq]) -> PackageSyncSpec { - let (entrypoints_to_keep, entrypoints_to_remove): ( - HashSet, - HashSet, - ) = self - .lock - .entrypoints - .iter() - .map(|id| { - self.get(id) - .expect("entrypoint not found in malformed lockfile.") - }) - .cloned() - .partition(|local_pkg| { - packages - .iter() - .any(|req| req.matches(&local_pkg.as_package_spec())) - }); - - let packages_to_keep: HashSet<&LocalPackage> = entrypoints_to_keep - .iter() - .flat_map(|local_pkg| self.get_all_dependencies(&local_pkg.id())) - .collect(); - - let to_add = packages - .iter() - .filter(|pkg| self.has_rock(pkg, None).is_none()) - .cloned() - .collect_vec(); - - let to_remove = entrypoints_to_remove - .iter() - .flat_map(|local_pkg| self.get_all_dependencies(&local_pkg.id())) - .filter(|dependency| !packages_to_keep.contains(dependency)) - .cloned() - .collect_vec(); - - PackageSyncSpec { to_add, to_remove } - } - - /// Return all dependencies of a package, including itself - fn get_all_dependencies(&self, id: &LocalPackageId) -> HashSet<&LocalPackage> { - let mut packages = HashSet::new(); - if let Some(local_pkg) = self.get(id) { - packages.insert(local_pkg); - packages.extend( - local_pkg - .dependencies() - .iter() - .flat_map(|id| self.get_all_dependencies(id)), - ); - } - packages - } - /// Creates a temporary, writeable lockfile which can never flush. pub fn into_temporary(self) -> Lockfile { Lockfile:: { @@ -773,6 +916,84 @@ impl Lockfile { //} } +impl ProjectLockfile { + pub fn new(filepath: PathBuf) -> io::Result> { + // Ensure that the lockfile exists + match File::options().create_new(true).write(true).open(&filepath) { + Ok(mut file) => { + write!( + file, + r#" + {{ + "dependencies": {{ + "entrypoints": [], + "rocks": {{}} + }}, + "version": "1.0.0" + }} + "# + )?; + } + Err(err) if err.kind() == ErrorKind::AlreadyExists => {} + Err(err) => return Err(err), + } + + let mut new: ProjectLockfile = + serde_json::from_str(&std::fs::read_to_string(&filepath)?)?; + + new.filepath = filepath; + + Ok(new) + } + + /// Creates a temporary, writeable project lockfile which can never flush. + pub fn into_temporary(self) -> ProjectLockfile { + ProjectLockfile:: { + _marker: PhantomData, + filepath: self.filepath, + version: self.version, + dependencies: self.dependencies, + test_dependencies: self.test_dependencies, + build_dependencies: self.build_dependencies, + } + } + + /// Converts the current lockfile into a writeable one, executes `cb` and flushes + /// the lockfile. + pub fn map_then_flush(&mut self, cb: F) -> Result + where + F: FnOnce(&mut ProjectLockfile) -> Result, + E: Error, + E: From, + { + let mut writeable_lockfile = self.clone().into_temporary(); + + let result = cb(&mut writeable_lockfile)?; + + writeable_lockfile.flush()?; + + Ok(result) + } + + /// Creates a project lockfile guard, flushing the lockfile automatically + /// once the guard goes out of scope. + pub fn write_guard(self) -> ProjectLockfileGuard { + ProjectLockfileGuard(self.into_temporary()) + } + + pub(crate) fn package_sync_spec( + &self, + packages: &[PackageReq], + deps: &LocalPackageLockType, + ) -> PackageSyncSpec { + match deps { + LocalPackageLockType::Regular => self.dependencies.package_sync_spec(packages), + LocalPackageLockType::Test => self.test_dependencies.package_sync_spec(packages), + LocalPackageLockType::Build => self.build_dependencies.package_sync_spec(packages), + } + } +} + impl Lockfile { pub fn add(&mut self, rock: &LocalPackage) { self.lock.rocks.insert(rock.id(), rock.clone()); @@ -795,23 +1016,48 @@ impl Lockfile { } pub(crate) fn remove(&mut self, target: &LocalPackage) { - self.remove_by_id(&target.id()) + self.lock.remove(target) } pub(crate) fn remove_by_id(&mut self, target: &LocalPackageId) { - self.lock.rocks.remove(target); - self.lock.entrypoints.retain(|x| x != target); + self.lock.remove_by_id(target) } - pub(crate) fn sync_lockfile(&mut self, other: &Lockfile) { - self.lock = other.lock.clone(); + pub(crate) fn sync(&mut self, lock: &LocalPackageLock) { + self.lock = lock.clone(); } // TODO: `fn entrypoints() -> Vec` } +impl ProjectLockfile { + pub(crate) fn remove(&mut self, target: &LocalPackage, deps: &LocalPackageLockType) { + match deps { + LocalPackageLockType::Regular => self.dependencies.remove(target), + LocalPackageLockType::Test => self.test_dependencies.remove(target), + LocalPackageLockType::Build => self.build_dependencies.remove(target), + } + } + + pub(crate) fn sync(&mut self, lock: &LocalPackageLock, deps: &LocalPackageLockType) { + match deps { + LocalPackageLockType::Regular => { + self.dependencies = lock.clone(); + } + LocalPackageLockType::Test => { + self.test_dependencies = lock.clone(); + } + LocalPackageLockType::Build => { + self.build_dependencies = lock.clone(); + } + } + } +} + pub struct LockfileGuard(Lockfile); +pub struct ProjectLockfileGuard(ProjectLockfile); + impl Serialize for LockfileGuard { fn serialize(&self, serializer: S) -> Result where @@ -821,6 +1067,15 @@ impl Serialize for LockfileGuard { } } +impl Serialize for ProjectLockfileGuard { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.0.serialize(serializer) + } +} + impl<'de> Deserialize<'de> for LockfileGuard { fn deserialize(deserializer: D) -> std::result::Result where @@ -832,6 +1087,17 @@ impl<'de> Deserialize<'de> for LockfileGuard { } } +impl<'de> Deserialize<'de> for ProjectLockfileGuard { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + Ok(ProjectLockfileGuard( + ProjectLockfile::::deserialize(deserializer)?, + )) + } +} + impl Deref for LockfileGuard { type Target = Lockfile; @@ -840,18 +1106,38 @@ impl Deref for LockfileGuard { } } +impl Deref for ProjectLockfileGuard { + type Target = ProjectLockfile; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + impl DerefMut for LockfileGuard { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } +impl DerefMut for ProjectLockfileGuard { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + impl Drop for LockfileGuard { fn drop(&mut self) { let _ = self.flush(); } } +impl Drop for ProjectLockfileGuard { + fn drop(&mut self) { + let _ = self.flush(); + } +} + #[cfg(feature = "lua")] impl mlua::UserData for Lockfile { fn add_methods>(methods: &mut M) { @@ -1003,7 +1289,7 @@ mod tests { PackageReq::parse("nonexistent").unwrap(), ]; - let sync_spec = lockfile.package_sync_spec(&packages); + let sync_spec = lockfile.lock.package_sync_spec(&packages); assert_eq!(sync_spec.to_add.len(), 1); @@ -1060,7 +1346,7 @@ mod tests { fn test_sync_spec_empty() { let lockfile = get_test_lockfile(); let packages = vec![]; - let sync_spec = lockfile.package_sync_spec(&packages); + let sync_spec = lockfile.lock.package_sync_spec(&packages); // Should remove all packages assert!(sync_spec.to_add.is_empty()); diff --git a/rocks-lib/src/luarocks/luarocks_installation.rs b/rocks-lib/src/luarocks/luarocks_installation.rs index eb811bc7..231d9481 100644 --- a/rocks-lib/src/luarocks/luarocks_installation.rs +++ b/rocks-lib/src/luarocks/luarocks_installation.rs @@ -74,7 +74,7 @@ pub struct LuaRocksInstallation { pub config: Config, } -const LUAROCKS_VERSION: &str = "3.11.1-1"; +pub(crate) const LUAROCKS_VERSION: &str = "3.11.1-1"; const LUAROCKS_ROCKSPEC: &str = " rockspec_format = '3.0' @@ -96,6 +96,14 @@ impl LuaRocksInstallation { Ok(luarocks_installation) } + pub fn tree(&self) -> &Tree { + &self.tree + } + + pub fn config(&self) -> &Config { + &self.config + } + pub async fn ensure_installed( &self, progress: &Progress, diff --git a/rocks-lib/src/operations/install.rs b/rocks-lib/src/operations/install.rs index d37fbf42..d579b0fd 100644 --- a/rocks-lib/src/operations/install.rs +++ b/rocks-lib/src/operations/install.rs @@ -168,7 +168,6 @@ async fn install( where { let lockfile = tree.lockfile()?; - let project_lockfile = project.map(|p| p.lockfile()).transpose()?; let tree = project.map_or(Ok(tree.clone()), |p| p.tree(config))?; install_impl( @@ -178,7 +177,6 @@ where config, &tree, lockfile, - project_lockfile, progress, ) .await @@ -193,7 +191,6 @@ async fn install_impl( config: &Config, tree: &Tree, mut lockfile: Lockfile, - project_lockfile: Option>, progress_arc: Arc>, ) -> Result, InstallError> { let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); @@ -300,16 +297,6 @@ async fn install_impl( Ok::<_, io::Error>(()) })?; - if let Some(mut project_lockfile) = project_lockfile { - project_lockfile.map_then_flush(|lockfile| { - installed_packages - .iter() - .for_each(|(id, pkg)| write_dependency(lockfile, id, pkg)); - - Ok::<_, io::Error>(()) - })?; - } - Ok(installed_packages.into_values().collect_vec()) } diff --git a/rocks-lib/src/operations/sync.rs b/rocks-lib/src/operations/sync.rs index 8a9a5215..b51652b7 100644 --- a/rocks-lib/src/operations/sync.rs +++ b/rocks-lib/src/operations/sync.rs @@ -3,7 +3,11 @@ use std::{io, sync::Arc}; use crate::{ build::BuildBehaviour, config::Config, - lockfile::{LocalPackage, Lockfile, LockfileIntegrityError, PackageSyncSpec, ReadOnly}, + lockfile::{ + LocalPackage, LocalPackageLockType, LockfileIntegrityError, PackageSyncSpec, PinnedState, + ProjectLockfile, ReadOnly, + }, + luarocks::luarocks_installation::LUAROCKS_VERSION, package::{PackageName, PackageReq}, progress::{MultiProgress, Progress}, tree::Tree, @@ -21,9 +25,9 @@ pub struct Sync<'a> { /// The tree to sync #[builder(start_fn)] tree: &'a Tree, - /// The lockfile to sync the tree with + /// The project lockfile to sync the tree with #[builder(start_fn)] - lockfile: &'a mut Lockfile, + project_lockfile: &'a mut ProjectLockfile, #[builder(start_fn)] config: &'a Config, #[builder(field)] @@ -33,6 +37,8 @@ pub struct Sync<'a> { packages: Option>, /// Whether to validate the integrity of installed packages. validate_integrity: Option, + /// Whether to pin newly added packages + pin: Option, } impl SyncBuilder<'_, State> @@ -53,17 +59,38 @@ where self.packages = Some(packages); self } + + fn add_package(&mut self, package: PackageReq) -> &Self { + match &mut self.packages { + Some(packages) => packages.push(package), + None => self.packages = Some(vec![package]), + } + self + } } impl SyncBuilder<'_, State> where State: sync_builder::State + sync_builder::IsComplete, { - pub async fn sync(self) -> Result { - do_sync(self._build()).await + pub async fn sync_dependencies(self) -> Result { + do_sync(self._build(), &LocalPackageLockType::Regular).await + } + + pub async fn sync_test_dependencies(mut self) -> Result { + let busted = PackageReq::new("busted".into(), None).unwrap(); + self.add_package(busted); + do_sync(self._build(), &LocalPackageLockType::Test).await + } + + pub async fn sync_build_dependencies(mut self) -> Result { + let luarocks = PackageReq::new("luarocks".into(), Some(LUAROCKS_VERSION.into())).unwrap(); + self.add_package(luarocks); + do_sync(self._build(), &LocalPackageLockType::Build).await } } +#[derive(Debug)] pub struct SyncReport { added: Vec, removed: Vec, @@ -81,34 +108,40 @@ pub enum SyncError { Integrity(PackageName, LockfileIntegrityError), } -async fn do_sync(args: Sync<'_>) -> Result { +async fn do_sync( + args: Sync<'_>, + lock_type: &LocalPackageLockType, +) -> Result { let progress = args.progress.unwrap_or(MultiProgress::new_arc()); + std::fs::create_dir_all(args.tree.root())?; let dest_lockfile = args.tree.lockfile()?; + let pin = args.pin.unwrap_or_default(); let package_sync_spec = match &args.packages { - Some(packages) => args.lockfile.package_sync_spec(packages), + Some(packages) => args.project_lockfile.package_sync_spec(packages, lock_type), None => PackageSyncSpec::default(), }; - args.lockfile.map_then_flush(|lockfile| -> io::Result<()> { - package_sync_spec - .to_remove - .iter() - .for_each(|pkg| lockfile.remove(pkg)); - Ok(()) - })?; + args.project_lockfile + .map_then_flush(|lockfile| -> io::Result<()> { + package_sync_spec + .to_remove + .iter() + .for_each(|pkg| lockfile.remove(pkg, lock_type)); + Ok(()) + })?; let mut report = SyncReport { added: Vec::new(), removed: Vec::new(), }; - for (id, local_package) in args.lockfile.rocks() { + for (id, local_package) in args.project_lockfile.rocks(lock_type) { if dest_lockfile.get(id).is_none() { report.added.push(local_package.clone()); } } for (id, local_package) in dest_lockfile.rocks() { - if args.lockfile.get(id).is_none() { + if args.project_lockfile.get(id, lock_type).is_none() { report.removed.push(local_package.clone()); } } @@ -121,10 +154,15 @@ async fn do_sync(args: Sync<'_>) -> Result { .map(|pkg| (BuildBehaviour::Force, pkg)) .collect_vec(); - let package_db = args.lockfile.clone().into(); + let package_db = args + .project_lockfile + .local_pkg_lock(lock_type) + .clone() + .into(); Install::new(args.tree, args.config) .package_db(package_db) .packages(packages_to_install) + .pin(pin) .progress(progress.clone()) .install() .await?; @@ -134,11 +172,9 @@ async fn do_sync(args: Sync<'_>) -> Result { if args.validate_integrity.unwrap_or(true) { for package in &report.added { - if package.name().to_string() != "say" { - dest_lockfile - .validate_integrity(package) - .map_err(|err| SyncError::Integrity(package.name().clone(), err))?; - } + dest_lockfile + .validate_integrity(package) + .map_err(|err| SyncError::Integrity(package.name().clone(), err))?; } } @@ -155,7 +191,7 @@ async fn do_sync(args: Sync<'_>) -> Result { .remove() .await?; - dest_lockfile.sync_lockfile(args.lockfile); + dest_lockfile.sync(args.project_lockfile.local_pkg_lock(lock_type)); if !package_sync_spec.to_add.is_empty() { // Install missing packages using the default package_db. @@ -164,18 +200,22 @@ async fn do_sync(args: Sync<'_>) -> Result { .into_iter() .map(|pkg| (BuildBehaviour::Force, pkg)); - Install::new(args.tree, args.config) + let added = Install::new(args.tree, args.config) .packages(missing_packages) + .pin(pin) .progress(progress.clone()) .install() .await?; + report.added.extend(added); + // Sync the newly added packages back to the source lockfile let dest_lockfile = args.tree.lockfile()?; - args.lockfile.map_then_flush(|lockfile| -> io::Result<()> { - lockfile.sync_lockfile(&dest_lockfile); - Ok(()) - })?; + args.project_lockfile + .map_then_flush(|lockfile| -> io::Result<()> { + lockfile.sync(dest_lockfile.local_pkg_lock(), lock_type); + Ok(()) + })?; } Ok(report) @@ -197,10 +237,9 @@ mod tests { println!("Skipping impure test"); return; } - let tree_path = - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-tree"); - let tree = Tree::new(tree_path.clone(), LuaVersion::Lua51).unwrap(); - let mut source_lockfile = tree.lockfile().unwrap(); + let project_lockfile_path = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/rocks.lock"); + let mut source_lockfile = ProjectLockfile::new(project_lockfile_path.clone()).unwrap(); let temp_dir = TempDir::new().unwrap(); let config = ConfigBuilder::new() .unwrap() @@ -209,11 +248,52 @@ mod tests { .unwrap(); let dest_tree = config.tree(LuaVersion::Lua51).unwrap(); let report = Sync::new(&dest_tree, &mut source_lockfile, &config) - .sync() + .sync_dependencies() + .await + .unwrap(); + assert!(report.removed.is_empty()); + assert!(!report.added.is_empty()); + + let lockfile_after_sync = ProjectLockfile::new(project_lockfile_path).unwrap(); + assert!(!lockfile_after_sync + .rocks(&LocalPackageLockType::Regular) + .is_empty()); + } + + #[tokio::test] + async fn test_sync_add_rocks_with_new_package() { + if std::env::var("ROCKS_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" { + println!("Skipping impure test"); + return; + } + let empty_lockfile_dir = TempDir::new().unwrap(); + let lockfile_path = empty_lockfile_dir.path().join("rocks.lock"); + let mut empty_lockfile = ProjectLockfile::new(lockfile_path.clone()).unwrap(); + let temp_dir = TempDir::new().unwrap(); + let config = ConfigBuilder::new() + .unwrap() + .tree(Some(temp_dir.path().into())) + .build() + .unwrap(); + let dest_tree = config.tree(LuaVersion::Lua51).unwrap(); + let report = Sync::new(&dest_tree, &mut empty_lockfile, &config) + .packages(vec![PackageReq::new("toml-edit".into(), None).unwrap()]) + .sync_dependencies() .await .unwrap(); assert!(report.removed.is_empty()); assert!(!report.added.is_empty()); + assert!(!report + .added + .iter() + .filter(|pkg| pkg.name().to_string() == "toml-edit") + .collect_vec() + .is_empty()); + + let lockfile_after_sync = ProjectLockfile::new(lockfile_path).unwrap(); + assert!(!lockfile_after_sync + .rocks(&LocalPackageLockType::Regular) + .is_empty()); } #[tokio::test] @@ -223,8 +303,8 @@ mod tests { let temp_dir = TempDir::new().unwrap(); temp_dir.copy_from(&tree_path, &["**"]).unwrap(); let empty_lockfile_dir = TempDir::new().unwrap(); - let mut empty_lockfile = - Lockfile::new(empty_lockfile_dir.path().join("lock.json")).unwrap(); + let lockfile_path = empty_lockfile_dir.path().join("rocks.lock"); + let mut empty_lockfile = ProjectLockfile::new(lockfile_path.clone()).unwrap(); let config = ConfigBuilder::new() .unwrap() .tree(Some(temp_dir.path().into())) @@ -232,10 +312,15 @@ mod tests { .unwrap(); let dest_tree = config.tree(LuaVersion::Lua51).unwrap(); let report = Sync::new(&dest_tree, &mut empty_lockfile, &config) - .sync() + .sync_dependencies() .await .unwrap(); assert!(!report.removed.is_empty()); assert!(report.added.is_empty()); + + let lockfile_after_sync = ProjectLockfile::new(lockfile_path).unwrap(); + assert!(lockfile_after_sync + .rocks(&LocalPackageLockType::Regular) + .is_empty()); } } diff --git a/rocks-lib/src/operations/test.rs b/rocks-lib/src/operations/test.rs index 9b1f7adc..b456a495 100644 --- a/rocks-lib/src/operations/test.rs +++ b/rocks-lib/src/operations/test.rs @@ -6,15 +6,15 @@ use crate::{ package::{PackageName, PackageReq, PackageVersionReqError}, path::Paths, progress::{MultiProgress, Progress}, - project::{rocks_toml::RocksTomlValidationError, Project}, - rockspec::{LuaVersionCompatibility, Rockspec}, + project::{rocks_toml::RocksTomlValidationError, Project, ProjectTreeError}, + rockspec::Rockspec, tree::Tree, }; use bon::Builder; use itertools::Itertools; use thiserror::Error; -use super::{Install, InstallError}; +use super::{Install, InstallError, Sync, SyncError}; #[derive(Builder)] #[builder(start_fn = new, finish_fn(name = _run, vis = ""))] @@ -27,6 +27,8 @@ pub struct Test<'a> { #[builder(field)] args: Vec, + no_lock: Option, + #[builder(default)] env: TestEnv, #[builder(default = MultiProgress::new_arc())] @@ -74,28 +76,66 @@ pub enum RunTestsError { TestFailure, #[error("failed to execute `{0}`: {1}")] RunCommandFailure(String, io::Error), - #[error("lua version not set! Please provide a version through `--lua-version ` or add it to your rockspec's dependencies.")] - LuaVersionUnset, #[error(transparent)] Io(#[from] io::Error), #[error(transparent)] + Tree(#[from] ProjectTreeError), + #[error(transparent)] RocksTomlValidation(#[from] RocksTomlValidationError), + #[error("failed to sync dependencies: {0}")] + Sync(#[from] SyncError), } async fn run_tests(test: Test<'_>) -> Result<(), RunTestsError> { let rocks = test.project.rocks().into_validated_rocks_toml()?; - let lua_version = match rocks.lua_version_matches(test.config) { - Ok(lua_version) => Ok(lua_version), - Err(_) => rocks - .test_lua_version() - .ok_or(RunTestsError::LuaVersionUnset), - }?; - let tree = test.config.tree(lua_version)?; + let project_tree = test.project.tree(test.config)?; + let test_tree = test.project.test_tree(test.config)?; + std::fs::create_dir_all(test_tree.root())?; // TODO(#204): Only ensure busted if running with busted (e.g. a .busted directory exists) - ensure_busted(&tree, test.config, test.progress.clone()).await?; - ensure_dependencies(&rocks, &tree, test.config, test.progress).await?; - let tree_root = &tree.root().clone(); - let paths = Paths::new(tree)?; + if test.no_lock.unwrap_or(false) { + ensure_dependencies( + &rocks, + &project_tree, + &test_tree, + test.config, + test.progress, + ) + .await?; + } else { + let mut lockfile = test.project.lockfile()?; + + let test_dependencies = rocks + .test_dependencies() + .current_platform() + .iter() + .filter(|req| !req.name().eq(&PackageName::new("lua".into()))) + .cloned() + .collect_vec(); + + Sync::new(&test_tree, &mut lockfile, test.config) + .progress(test.progress.clone()) + .packages(test_dependencies) + .sync_test_dependencies() + .await?; + + let dependencies = rocks + .dependencies() + .current_platform() + .iter() + .filter(|req| !req.name().eq(&PackageName::new("lua".into()))) + .cloned() + .collect_vec(); + + Sync::new(&project_tree, &mut lockfile, test.config) + .progress(test.progress.clone()) + .packages(dependencies) + .sync_dependencies() + .await?; + } + let test_tree_root = &test_tree.root().clone(); + let mut paths = Paths::new(project_tree)?; + let test_tree_paths = Paths::new(test_tree)?; + paths.prepend(&test_tree_paths); let mut command = Command::new("busted"); let mut command = command .current_dir(test.project.root()) @@ -106,7 +146,7 @@ async fn run_tests(test: Test<'_>) -> Result<(), RunTestsError> { if let TestEnv::Pure = test.env { // isolate the test runner from the user's own config/data files // by initialising empty HOME and XDG base directory paths - let home = tree_root.join("home"); + let home = test_tree_root.join("home"); let xdg = home.join("xdg"); let _ = std::fs::remove_dir_all(&home); let xdg_config_home = xdg.join("config"); @@ -164,18 +204,42 @@ pub async fn ensure_busted( /// This defaults to the local project tree if cwd is a project root. async fn ensure_dependencies( rockspec: &impl Rockspec, - tree: &Tree, + project_tree: &Tree, + test_tree: &Tree, config: &Config, progress: Arc>, ) -> Result<(), InstallTestDependenciesError> { - let dependencies = rockspec + ensure_busted(test_tree, config, progress.clone()).await?; + let test_dependencies = rockspec .test_dependencies() .current_platform() .iter() - .chain(rockspec.dependencies().current_platform()) .filter(|req| !req.name().eq(&PackageName::new("lua".into()))) .filter_map(|req| { - let build_behaviour = if tree + let build_behaviour = if test_tree + .match_rocks(req) + .is_ok_and(|matches| matches.is_found()) + { + Some(BuildBehaviour::Force) + } else { + None + }; + build_behaviour.map(|it| (it, req.to_owned())) + }); + + Install::new(test_tree, config) + .packages(test_dependencies) + .progress(progress.clone()) + .install() + .await?; + + let dependencies = rockspec + .dependencies() + .current_platform() + .iter() + .filter(|req| !req.name().eq(&PackageName::new("lua".into()))) + .filter_map(|req| { + let build_behaviour = if project_tree .match_rocks(req) .is_ok_and(|matches| matches.is_found()) { @@ -186,7 +250,7 @@ async fn ensure_dependencies( build_behaviour.map(|it| (it, req.to_owned())) }); - Install::new(tree, config) + Install::new(project_tree, config) .packages(dependencies) .progress(progress) .install() diff --git a/rocks-lib/src/project/mod.rs b/rocks-lib/src/project/mod.rs index 4b5c49dc..1fc93aa0 100644 --- a/rocks-lib/src/project/mod.rs +++ b/rocks-lib/src/project/mod.rs @@ -10,7 +10,7 @@ use thiserror::Error; use crate::{ config::{Config, LuaVersion}, - lockfile::{Lockfile, ReadOnly}, + lockfile::{ProjectLockfile, ReadOnly}, lua_rockspec::{ ExternalDependencySpec, LuaRockspec, LuaVersionError, PartialLuaRockspec, PartialRockspecError, RockSourceSpec, RockspecError, @@ -66,9 +66,9 @@ pub enum ProjectTreeError { #[derive(Clone, Debug)] pub struct Project { - /// The path where the `project.rockspec` resides. + /// The path where the `rocks.toml` resides. root: PathBuf, - /// The parsed rockspec. + /// The parsed rocks.toml. rocks: RocksToml, } @@ -125,16 +125,16 @@ impl Project { self.root.join("rocks.lock") } - /// Get the `rocks.lock` lockfile in the project root, if present. - pub fn lockfile(&self) -> Result, io::Error> { - Lockfile::new(self.lockfile_path()) + /// Get the `rocks.lock` lockfile in the project root. + pub fn lockfile(&self) -> Result, io::Error> { + ProjectLockfile::new(self.lockfile_path()) } /// Get the `rocks.lock` lockfile in the project root, if present. - pub fn try_lockfile(&self) -> Result>, io::Error> { + pub fn try_lockfile(&self) -> Result>, io::Error> { let path = self.lockfile_path(); if path.is_file() { - Ok(Some(Lockfile::new(path)?)) + Ok(Some(ProjectLockfile::new(path)?)) } else { Ok(None) } @@ -173,9 +173,17 @@ impl Project { Ok(rocks) } + fn tree_root_dir(&self) -> PathBuf { + self.root.join(".rocks") + } + pub fn tree(&self, config: &Config) -> Result { + Ok(Tree::new(self.tree_root_dir(), self.lua_version(config)?)?) + } + + pub fn test_tree(&self, config: &Config) -> Result { Ok(Tree::new( - self.root.join(".rocks"), + self.tree_root_dir().join("test_dependencies"), self.lua_version(config)?, )?) } diff --git a/rocks-lib/src/remote_package_db/mod.rs b/rocks-lib/src/remote_package_db/mod.rs index 0bf450d9..b606577f 100644 --- a/rocks-lib/src/remote_package_db/mod.rs +++ b/rocks-lib/src/remote_package_db/mod.rs @@ -1,6 +1,6 @@ use crate::{ config::{Config, ConfigError}, - lockfile::{Lockfile, LockfileIntegrityError, ReadOnly}, + lockfile::{LocalPackageLock, LockfileIntegrityError}, manifest::{Manifest, ManifestError}, package::{ PackageName, PackageReq, PackageSpec, PackageVersion, RemotePackage, @@ -17,7 +17,7 @@ pub struct RemotePackageDB(Impl); #[derive(Clone)] enum Impl { LuarocksManifests(Vec), - Lockfile(Lockfile), + Lock(LocalPackageLock), } #[derive(Error, Debug)] @@ -79,7 +79,7 @@ impl RemotePackageDB { Some(package) => Ok(package), None => Err(SearchError::RockNotFound(package_req.clone())), }, - Impl::Lockfile(lockfile) => { + Impl::Lock(lockfile) => { match lockfile.has_rock(package_req, filter).map(|local_package| { RemotePackage::new( PackageSpec::new(local_package.spec.name, local_package.spec.version), @@ -121,7 +121,7 @@ impl RemotePackageDB { }) }) .collect(), - Impl::Lockfile(lockfile) => lockfile + Impl::Lock(lockfile) => lockfile .rocks() .iter() .filter_map(|(_, package)| { @@ -163,8 +163,8 @@ impl From for RemotePackageDB { } } -impl From> for RemotePackageDB { - fn from(lockfile: Lockfile) -> Self { - Self(Impl::Lockfile(lockfile)) +impl From for RemotePackageDB { + fn from(lock: LocalPackageLock) -> Self { + Self(Impl::Lock(lock)) } } diff --git a/rocks-lib/tests/test.rs b/rocks-lib/tests/test.rs index e753137d..6dba6b5a 100644 --- a/rocks-lib/tests/test.rs +++ b/rocks-lib/tests/test.rs @@ -1,11 +1,15 @@ use std::path::PathBuf; +use assert_fs::prelude::PathCopy; use rocks_lib::{config::ConfigBuilder, operations::Test, project::Project}; #[tokio::test] async fn run_busted_test() { let project_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-project-busted"); + let temp_dir = assert_fs::TempDir::new().unwrap(); + temp_dir.copy_from(&project_root, &["**"]).unwrap(); + let project_root = temp_dir.path(); let project: Project = Project::from(project_root).unwrap().unwrap(); let tree_root = project.root().to_path_buf().join(".rocks"); let _ = std::fs::remove_dir_all(&tree_root); @@ -16,3 +20,25 @@ async fn run_busted_test() { .unwrap(); Test::new(project, &config).run().await.unwrap(); } + +#[tokio::test] +async fn run_busted_test_no_lock() { + let project_root = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-project-busted"); + let temp_dir = assert_fs::TempDir::new().unwrap(); + temp_dir.copy_from(&project_root, &["**"]).unwrap(); + let project_root = temp_dir.path(); + let project: Project = Project::from(project_root).unwrap().unwrap(); + let tree_root = project.root().to_path_buf().join(".rocks"); + let _ = std::fs::remove_dir_all(&tree_root); + let config = ConfigBuilder::new() + .unwrap() + .tree(Some(tree_root)) + .build() + .unwrap(); + Test::new(project, &config) + .no_lock(true) + .run() + .await + .unwrap(); +}