From a4add1012a7f6bd992da28cb4662ec17a263cffe Mon Sep 17 00:00:00 2001 From: Marc Jakobi Date: Sat, 4 Jan 2025 22:23:21 +0100 Subject: [PATCH] feat!(rocks-bin/build): project lockfiles --- rocks-bin/src/build.rs | 101 ++++--------- rocks-bin/src/install_rockspec.rs | 84 +++++++++++ rocks-bin/src/lib.rs | 7 +- rocks-bin/src/main.rs | 9 +- rocks-lib/src/lockfile/mod.rs | 85 ++++++++++- rocks-lib/src/manifest/mod.rs | 12 +- rocks-lib/src/operations/install.rs | 23 ++- rocks-lib/src/operations/lockfile_update.rs | 98 ++++++++++++ rocks-lib/src/operations/mod.rs | 2 + rocks-lib/src/operations/resolve.rs | 3 +- rocks-lib/src/package/mod.rs | 8 + rocks-lib/src/package/outdated.rs | 4 +- rocks-lib/src/project/mod.rs | 31 +++- rocks-lib/src/remote_package_db/mod.rs | 158 +++++++++++++------- rocks-lib/src/rockspec/platform.rs | 8 + rocks-lib/src/tree/mod.rs | 7 +- 16 files changed, 490 insertions(+), 150 deletions(-) create mode 100644 rocks-bin/src/install_rockspec.rs create mode 100644 rocks-lib/src/operations/lockfile_update.rs diff --git a/rocks-bin/src/build.rs b/rocks-bin/src/build.rs index 5e887860..33bb544f 100644 --- a/rocks-bin/src/build.rs +++ b/rocks-bin/src/build.rs @@ -1,7 +1,6 @@ -use eyre::eyre; -use inquire::Confirm; +use eyre::{Context, OptionExt}; use itertools::Itertools; -use std::{path::PathBuf, sync::Arc}; +use std::sync::Arc; use clap::Args; use eyre::Result; @@ -10,88 +9,38 @@ use rocks_lib::{ config::Config, lockfile::PinnedState, operations::Install, - package::{PackageName, PackageReq}, + package::PackageName, progress::MultiProgress, - rockspec::Rockspec, - tree::{RockMatches, Tree}, + project::Project, + remote_package_db::RemotePackageDB, }; #[derive(Args, Default)] pub struct Build { - rockspec_path: Option, - + /// Whether to pin the dependencies. #[arg(long)] pin: bool, + /// Ignore the project's existing lockfile. + #[arg(long)] + ignore_lockfile: bool, + + /// Do not create a lockfile. #[arg(long)] - force: bool, + no_lock: bool, } pub async fn build(data: Build, config: Config) -> Result<()> { + let project = Project::current()?.ok_or_eyre("Not in a project!")?; let pin = PinnedState::from(data.pin); - - let rockspec_path = data.rockspec_path.map_or_else(|| { - // Try to infer the rockspec the user meant. - - let cwd = std::env::current_dir()?; - - let rockspec_paths = walkdir::WalkDir::new(cwd) - .max_depth(1) - .same_file_system(true) - .into_iter() - .filter_map(|entry| entry.ok()) - .filter(|entry| { - entry.file_type().is_file() - && entry.path().extension().map(|ext| ext.to_str()) == Some(Some("rockspec")) - }) - .collect_vec(); - - let rockspec_count = rockspec_paths.len(); - - match rockspec_count { - 0 => Err(eyre!("No rockspec files found in the current directory!")), - 1 => Ok(rockspec_paths.first().unwrap().clone().into_path()), - _ => Err(eyre!("Could not infer the rockspec to use! There are {} rockspecs in the current directory, please provide a path to the one you'd like to use.", rockspec_count)), - } - }, Ok)?; - - if rockspec_path - .extension() - .map(|ext| ext != "rockspec") - .unwrap_or(true) - { - return Err(eyre!("Provided path is not a valid rockspec!")); - } - - let rockspec = std::fs::read_to_string(rockspec_path)?; - let rockspec = Rockspec::new(&rockspec)?; - - let lua_version = rockspec.lua_version_from_config(&config)?; - - let tree = Tree::new(config.tree().clone(), lua_version)?; - - let build_behaviour = match tree.match_rocks_and( - &PackageReq::new( - rockspec.package.to_string(), - Some(rockspec.version.to_string()), - )?, - |rock| pin == rock.pinned(), - )? { - RockMatches::Single(_) | RockMatches::Many(_) if !data.force => { - if Confirm::new(&format!( - "Package {} already exists. Overwrite?", - rockspec.package, - )) - .with_default(false) - .prompt()? - { - BuildBehaviour::Force - } else { - return Ok(()); - } - } - _ => BuildBehaviour::from(data.force), + let package_db = match project.lockfile()? { + None => RemotePackageDB::from_config(&config).await?, + Some(_) if data.ignore_lockfile => RemotePackageDB::from_config(&config).await?, + Some(lockfile) => lockfile.into(), }; + let rockspec = project.new_local_rockspec(); + let lua_version = rockspec.lua_version_from_config(&config)?; + let tree = project.tree(lua_version)?; // Ensure all dependencies are installed first let dependencies = rockspec @@ -110,18 +59,24 @@ pub async fn build(data: Build, config: Config) -> Result<()> { tree.match_rocks(req) .is_ok_and(|rock_match| rock_match.is_found()) }) - .map(|dep| (build_behaviour, dep.to_owned())); + .map(|dep| (BuildBehaviour::NoForce, dep.to_owned())); Install::new(&config) .packages(dependencies_to_install) .pin(pin) + .package_db(package_db) .progress(progress_arc) .install() .await?; + if !data.no_lock { + std::fs::copy(tree.lockfile_path(), project.lockfile_path()) + .wrap_err("error copying the project lockfile")?; + } + build::Build::new(&rockspec, &config, &progress.map(|p| p.new_bar())) .pin(pin) - .behaviour(build_behaviour) + .behaviour(BuildBehaviour::Force) .build() .await?; diff --git a/rocks-bin/src/install_rockspec.rs b/rocks-bin/src/install_rockspec.rs new file mode 100644 index 00000000..d3da9baa --- /dev/null +++ b/rocks-bin/src/install_rockspec.rs @@ -0,0 +1,84 @@ +use eyre::{eyre, Context}; +use itertools::Itertools; +use std::{path::PathBuf, sync::Arc}; + +use clap::Args; +use eyre::Result; +use rocks_lib::{ + build::{self, BuildBehaviour}, + config::Config, + lockfile::PinnedState, + operations::Install, + package::PackageName, + progress::MultiProgress, + project::Project, + rockspec::Rockspec, + tree::Tree, +}; + +#[derive(Args, Default)] +pub struct InstallRockspec { + /// The path to the RockSpec file to install + rockspec_path: PathBuf, + + /// Whether to pin the installed package and dependencies. + #[arg(long)] + pin: bool, +} + +pub async fn install_rockspec(data: InstallRockspec, config: Config) -> Result<()> { + let pin = PinnedState::from(data.pin); + let project_opt = Project::current()?; + let path = data.rockspec_path; + + if path + .extension() + .map(|ext| ext != "rockspec") + .unwrap_or(true) + { + return Err(eyre!("Provided path is not a valid rockspec!")); + } + let content = std::fs::read_to_string(path)?; + let rockspec = Rockspec::new(&content)?; + let lua_version = rockspec.lua_version_from_config(&config)?; + let tree = Tree::new(config.tree().clone(), lua_version)?; + + // Ensure all dependencies are installed first + let dependencies = rockspec + .dependencies + .current_platform() + .iter() + .filter(|package| !package.name().eq(&PackageName::new("lua".into()))) + .collect_vec(); + + let progress_arc = MultiProgress::new_arc(); + let progress = Arc::clone(&progress_arc); + + 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.to_owned())); + + Install::new(&config) + .packages(dependencies_to_install) + .pin(pin) + .progress(progress_arc) + .install() + .await?; + + if let Some(project) = project_opt { + std::fs::copy(tree.lockfile_path(), project.lockfile_path()) + .wrap_err("error creating project lockfile.")?; + } + + build::Build::new(&rockspec, &config, &progress.map(|p| p.new_bar())) + .pin(pin) + .behaviour(BuildBehaviour::Force) + .build() + .await?; + + Ok(()) +} diff --git a/rocks-bin/src/lib.rs b/rocks-bin/src/lib.rs index 9ce17292..30510364 100644 --- a/rocks-bin/src/lib.rs +++ b/rocks-bin/src/lib.rs @@ -7,6 +7,7 @@ use debug::Debug; use download::Download; use info::Info; use install::Install; +use install_rockspec::InstallRockspec; use list::ListCmd; use outdated::Outdated; use path::Path; @@ -30,6 +31,7 @@ pub mod format; pub mod info; pub mod install; pub mod install_lua; +pub mod install_rockspec; pub mod list; pub mod outdated; pub mod path; @@ -110,7 +112,7 @@ pub struct Cli { pub enum Commands { /// [UNIMPLEMENTED] Add a dependency to the current project. Add, - /// Build/compile a rock. + /// Build/compile a project. Build(Build), /// Runs `luacheck` in the current project. Check, @@ -131,6 +133,9 @@ pub enum Commands { /// Install a rock for use on the system. #[command(arg_required_else_help = true)] Install(Install), + /// Install a local RockSpec for use on the system. + #[command(arg_required_else_help = true)] + InstallRockspec(InstallRockspec), /// Manually install and manage Lua headers for various Lua versions. InstallLua, /// [UNIMPLEMENTED] Check syntax of a rockspec. diff --git a/rocks-bin/src/main.rs b/rocks-bin/src/main.rs index c5366c95..b4bddfd8 100644 --- a/rocks-bin/src/main.rs +++ b/rocks-bin/src/main.rs @@ -4,8 +4,8 @@ use clap::Parser; use rocks::{ build, check, debug::Debug, - download, fetch, format, info, install, install_lua, list, outdated, path, pin, project, purge, - remove, run, run_lua, search, test, unpack, update, + download, fetch, format, info, install, install_lua, install_rockspec, list, outdated, path, + pin, project, purge, remove, run, run_lua, search, test, unpack, update, upload::{self}, Cli, Commands, }; @@ -56,6 +56,11 @@ async fn main() { Commands::List(list_data) => list::list_installed(list_data, config).unwrap(), Commands::Lua(run_lua) => run_lua::run_lua(run_lua, config).await.unwrap(), Commands::Install(install_data) => install::install(install_data, config).await.unwrap(), + Commands::InstallRockspec(install_data) => { + install_rockspec::install_rockspec(install_data, config) + .await + .unwrap() + } Commands::Outdated(outdated) => outdated::outdated(outdated, config).await.unwrap(), Commands::InstallLua => install_lua::install_lua(config).await.unwrap(), Commands::Fmt => format::format().unwrap(), diff --git a/rocks-lib/src/lockfile/mod.rs b/rocks-lib/src/lockfile/mod.rs index d12f8b8b..7f52d638 100644 --- a/rocks-lib/src/lockfile/mod.rs +++ b/rocks-lib/src/lockfile/mod.rs @@ -9,7 +9,8 @@ use ssri::Integrity; use thiserror::Error; use crate::package::{ - PackageName, PackageReq, PackageSpec, PackageVersion, PackageVersionReq, PackageVersionReqError, + PackageName, PackageReq, PackageSpec, PackageVersion, PackageVersionReq, + PackageVersionReqError, RemotePackageTypeFilterSpec, }; use crate::remote_package_source::RemotePackageSource; @@ -28,6 +29,15 @@ impl Default for PinnedState { } } +impl Display for PinnedState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self { + PinnedState::Unpinned => "unpinned".fmt(f), + PinnedState::Pinned => "pinned".fmt(f), + } + } +} + impl From for PinnedState { fn from(value: bool) -> Self { if value { @@ -184,7 +194,7 @@ impl LocalPackageSpec { #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct LocalPackage { pub(crate) spec: LocalPackageSpec, - source: RemotePackageSource, + pub(crate) source: RemotePackageSource, hashes: LocalPackageHashes, } @@ -423,6 +433,16 @@ pub struct Lockfile { entrypoints: Vec, } +#[derive(Error, Debug)] +pub enum LockfileIntegrityError { + #[error("rockspec integirty mismatch.\nExpected: {expected}\nBut got: {got}")] + RockspecIntegrityMismatch { expected: Integrity, got: Integrity }, + #[error("source integrity mismatch.\nExpected: {expected}\nBut got: {got}")] + SourceIntegrityMismatch { expected: Integrity, got: Integrity }, + #[error("package {0} version {1} with pinned state {2} and constraint {3} not found in the lockfile.")] + PackageNotFound(PackageName, PackageVersion, PinnedState, String), +} + impl Lockfile { pub fn new(filepath: PathBuf) -> io::Result { // Ensure that the lockfile exists @@ -524,17 +544,76 @@ impl Lockfile { .into_group_map() } - pub(crate) fn has_rock(&self, req: &PackageReq) -> Option { + 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() } + + /// Validate the integrity of an installed package with the entry in this lockfile. + pub(crate) fn validate_integrity( + &self, + package: &LocalPackage, + ) -> Result<(), LockfileIntegrityError> { + match self.get(&package.id()) { + Some(expected_package) => { + if package + .hashes + .rockspec + .matches(&expected_package.hashes.rockspec) + .is_none() + { + return Err(LockfileIntegrityError::RockspecIntegrityMismatch { + expected: expected_package.hashes.rockspec.clone(), + got: package.hashes.rockspec.clone(), + }); + } + if package + .hashes + .source + .matches(&expected_package.hashes.source) + .is_none() + { + return Err(LockfileIntegrityError::RockspecIntegrityMismatch { + expected: expected_package.hashes.source.clone(), + got: package.hashes.source.clone(), + }); + } + Ok(()) + } + None => Err(LockfileIntegrityError::PackageNotFound( + package.name().clone(), + package.version().clone(), + package.spec.pinned, + package + .spec + .constraint + .clone() + .unwrap_or("UNCONSTRAINED".into()), + )), + } + } } impl Drop for Lockfile { diff --git a/rocks-lib/src/manifest/mod.rs b/rocks-lib/src/manifest/mod.rs index 8ecabdf0..0037e3ca 100644 --- a/rocks-lib/src/manifest/mod.rs +++ b/rocks-lib/src/manifest/mod.rs @@ -141,14 +141,6 @@ impl ManifestMetadata { self.repository.contains_key(rock_name) } - pub fn latest_version(&self, rock_name: &PackageName) -> Option<&PackageVersion> { - if !self.has_rock(rock_name) { - return None; - } - - self.repository[rock_name].keys().sorted().last() - } - pub fn latest_match( &self, lua_package_req: &PackageReq, @@ -241,7 +233,9 @@ impl Manifest { pub fn metadata(&self) -> &ManifestMetadata { &self.metadata } - pub fn search( + + /// Find a package that matches the requirement, returning the latest match + pub fn find( &self, package_req: &PackageReq, filter: Option, diff --git a/rocks-lib/src/operations/install.rs b/rocks-lib/src/operations/install.rs index face5708..271ad2f3 100644 --- a/rocks-lib/src/operations/install.rs +++ b/rocks-lib/src/operations/install.rs @@ -13,7 +13,7 @@ use crate::{ }, package::{PackageName, PackageReq}, progress::{MultiProgress, Progress, ProgressBar}, - remote_package_db::{RemotePackageDB, RemotePackageDBError}, + remote_package_db::{RemotePackageDB, RemotePackageDBError, RemotePackageDbIntegrityError}, rockspec::{BuildBackendSpec, LuaVersionError}, tree::Tree, }; @@ -124,6 +124,8 @@ pub enum InstallError { RemotePackageDB(#[from] RemotePackageDBError), #[error("failed to install pre-built rock {0}: {1}")] InstallBinaryRockError(PackageName, InstallBinaryRockError), + #[error("integrity error for package {0}: {1}\n")] + Integrity(PackageName, RemotePackageDbIntegrityError), } async fn install( @@ -138,7 +140,15 @@ 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; + let result = install_impl( + packages, + pin, + Arc::new(package_db), + config, + &mut lockfile, + progress, + ) + .await; lockfile.flush()?; result } @@ -146,7 +156,7 @@ where async fn install_impl( packages: Vec<(BuildBehaviour, PackageReq)>, pin: PinnedState, - package_db: RemotePackageDB, + package_db: Arc, config: &Config, lockfile: &mut Lockfile, progress_arc: Arc>, @@ -157,7 +167,7 @@ async fn install_impl( tx, packages, pin, - Arc::new(package_db), + package_db.clone(), Arc::new(lockfile.clone()), config, progress_arc.clone(), @@ -174,6 +184,7 @@ async fn install_impl( let progress_arc = progress_arc.clone(); let downloaded_rock = install_spec.downloaded_rock; let config = config.clone(); + let package_db = Arc::clone(&package_db); tokio::spawn(async move { let rockspec = downloaded_rock.rockspec(); @@ -218,6 +229,10 @@ async fn install_impl( ), }; + package_db + .validate_integrity(&pkg) + .map_err(|err| InstallError::Integrity(pkg.name().clone(), err))?; + Ok::<_, InstallError>((pkg.id(), pkg)) }) })) diff --git a/rocks-lib/src/operations/lockfile_update.rs b/rocks-lib/src/operations/lockfile_update.rs new file mode 100644 index 00000000..70cc21ba --- /dev/null +++ b/rocks-lib/src/operations/lockfile_update.rs @@ -0,0 +1,98 @@ +use bon::Builder; +use itertools::Itertools; +use std::{collections::HashMap, sync::Arc}; +use thiserror::Error; + +use crate::{ + build::BuildBehaviour, + config::Config, + lockfile::{Lockfile, PinnedState}, + operations::get_all_dependencies, + package::PackageReq, + progress::{MultiProgress, Progress}, + remote_package_db::{RemotePackageDB, RemotePackageDBError}, +}; + +use super::SearchAndDownloadError; + +/// A rocks lockfile updater. +#[derive(Builder)] +#[builder(start_fn = new, finish_fn(name = _update, vis = ""))] +pub struct LockfileUpdate<'a> { + #[builder(start_fn)] + lockfile: &'a mut Lockfile, + + #[builder(start_fn)] + config: &'a Config, + + #[builder(field)] + packages: Vec, + + package_db: Option, + + #[builder(default)] + pin: PinnedState, + + #[builder(default = MultiProgress::new_arc())] + progress: Arc>, +} + +impl<'a, State: lockfile_update_builder::State> LockfileUpdateBuilder<'a, State> { + pub fn package(mut self, package: PackageReq) -> Self { + self.packages.push(package); + self + } + + pub fn packages(mut self, packages: impl IntoIterator) -> Self { + self.packages.extend(packages); + self + } + + /// Add packages that are not present in the lockfile + pub async fn add_missing_packages(self) -> Result<(), LockfileUpdateError> { + do_add_missing_packages(self._update()).await + } +} + +#[derive(Error, Debug)] +pub enum LockfileUpdateError { + #[error("error initialising remote package DB: {0}")] + RemotePackageDB(#[from] RemotePackageDBError), + #[error(transparent)] + SearchAndDownload(#[from] SearchAndDownloadError), +} + +async fn do_add_missing_packages(update: LockfileUpdate<'_>) -> Result<(), LockfileUpdateError> { + let package_db = match update.package_db { + Some(db) => db, + None => RemotePackageDB::from_config(update.config).await?, + }; + let mut lockfile = update.lockfile; + let packages_to_add = update + .packages + .iter() + .filter(|pkg| lockfile.has_rock(pkg, None).is_none()) + .map(|pkg| (BuildBehaviour::NoForce, pkg.clone())) + .collect_vec(); + + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); + get_all_dependencies( + tx, + packages_to_add, + update.pin, + Arc::new(package_db), + Arc::new(lockfile.clone()), + update.config, + update.progress, + ) + .await?; + + let mut all_packages = HashMap::with_capacity(rx.len()); + while let Some(dep) = rx.recv().await { + all_packages.insert(dep.spec.id(), dep); + } + + // TODO: download package sources to memory and compute hashes + + todo!() +} diff --git a/rocks-lib/src/operations/mod.rs b/rocks-lib/src/operations/mod.rs index 632639bb..4abe7b99 100644 --- a/rocks-lib/src/operations/mod.rs +++ b/rocks-lib/src/operations/mod.rs @@ -10,6 +10,7 @@ mod run; mod test; mod unpack; mod update; +mod lockfile_update; pub use download::*; pub use fetch::*; @@ -20,5 +21,6 @@ pub use run::*; pub use test::*; pub use unpack::*; pub use update::*; +pub use lockfile_update::*; pub(crate) use resolve::*; diff --git a/rocks-lib/src/operations/resolve.rs b/rocks-lib/src/operations/resolve.rs index 542676f6..7acb6d3d 100644 --- a/rocks-lib/src/operations/resolve.rs +++ b/rocks-lib/src/operations/resolve.rs @@ -39,7 +39,8 @@ pub(crate) async fn get_all_dependencies( .into_iter() // Exclude packages that are already installed .filter(|(build_behaviour, package)| { - build_behaviour == &BuildBehaviour::Force || lockfile.has_rock(package).is_none() + build_behaviour == &BuildBehaviour::Force + || lockfile.has_rock(package, None).is_none() }) .map(|(build_behaviour, package)| { let config = config.clone(); diff --git a/rocks-lib/src/package/mod.rs b/rocks-lib/src/package/mod.rs index 2fdc64c7..1824d176 100644 --- a/rocks-lib/src/package/mod.rs +++ b/rocks-lib/src/package/mod.rs @@ -204,6 +204,14 @@ impl From for PackageReq { } } +impl From for PackageReq { + fn from(name: PackageName) -> Self { + Self { + name, + version_req: PackageVersionReq::default(), + } + } +} #[cfg(feature = "lua")] impl mlua::UserData for PackageReq { fn add_methods>(methods: &mut M) { diff --git a/rocks-lib/src/package/outdated.rs b/rocks-lib/src/package/outdated.rs index 35119b16..491f7d1c 100644 --- a/rocks-lib/src/package/outdated.rs +++ b/rocks-lib/src/package/outdated.rs @@ -28,8 +28,8 @@ impl PackageSpec { .latest_version(&self.name) .ok_or_else(|| RockNotFound(self.name.clone()))?; - if self.version < *latest_version { - Ok(Some(latest_version.to_owned())) + if self.version < latest_version { + Ok(Some(latest_version)) } else { Ok(None) } diff --git a/rocks-lib/src/project/mod.rs b/rocks-lib/src/project/mod.rs index 10b4eda5..24fd87a8 100644 --- a/rocks-lib/src/project/mod.rs +++ b/rocks-lib/src/project/mod.rs @@ -7,7 +7,8 @@ use thiserror::Error; use crate::{ config::LuaVersion, - rockspec::{Rockspec, RockspecError}, + lockfile::Lockfile, + rockspec::{RockSourceSpec, Rockspec, RockspecError}, tree::Tree, }; @@ -59,9 +60,22 @@ impl Project { None => Ok(None), } } -} -impl Project { + /// Get the `rocks.lock` lockfile path. + pub fn lockfile_path(&self) -> PathBuf { + self.root.join("rocks.lock") + } + + /// Get the `rocks.lock` lockfile in the project root, if present. + pub fn lockfile(&self) -> Result, ProjectError> { + let path = self.lockfile_path(); + if path.is_file() { + Ok(Some(Lockfile::new(path)?)) + } else { + Ok(None) + } + } + pub fn root(&self) -> &Path { &self.root } @@ -70,6 +84,17 @@ impl Project { &self.rockspec } + /// Create a RockSpec with the source set to the project root. + pub fn new_local_rockspec(&self) -> Rockspec { + let mut rockspec = self.rockspec().clone(); + let mut source = rockspec.source.current_platform().clone(); + source.source_spec = RockSourceSpec::File(self.root().to_path_buf()); + source.archive_name = None; + source.integrity = None; + rockspec.source.current_platform_set(source); + rockspec + } + pub fn tree(&self, lua_version: LuaVersion) -> io::Result { Tree::new(self.root.join(".rocks"), lua_version) } diff --git a/rocks-lib/src/remote_package_db/mod.rs b/rocks-lib/src/remote_package_db/mod.rs index dd2b59dc..ccfa5464 100644 --- a/rocks-lib/src/remote_package_db/mod.rs +++ b/rocks-lib/src/remote_package_db/mod.rs @@ -1,5 +1,6 @@ use crate::{ config::Config, + lockfile::{LocalPackage, Lockfile, LockfileIntegrityError}, manifest::{Manifest, ManifestError}, package::{ PackageName, PackageReq, PackageSpec, PackageVersion, RemotePackage, @@ -7,11 +8,17 @@ use crate::{ }, progress::{Progress, ProgressBar}, }; -use itertools::Itertools as _; +use itertools::Itertools; use thiserror::Error; #[derive(Clone)] -pub struct RemotePackageDB(Vec); +pub struct RemotePackageDB(Impl); + +#[derive(Clone)] +enum Impl { + LuarocksManifests(Vec), + Lockfile(Lockfile), +} #[derive(Error, Debug)] pub enum RemotePackageDBError { @@ -25,10 +32,18 @@ pub enum SearchError { Mlua(#[from] mlua::Error), #[error("no rock that matches '{0}' found")] RockNotFound(PackageReq), + #[error("no rock that matches '{0}' found in the lockfile.")] + RockNotFoundInLockfile(PackageReq), #[error("error when pulling manifest: {0}")] Manifest(#[from] ManifestError), } +#[derive(Error, Debug)] +pub enum RemotePackageDbIntegrityError { + #[error(transparent)] + Lockfile(#[from] LockfileIntegrityError), +} + impl RemotePackageDB { pub async fn from_config(config: &Config) -> Result { let mut manifests = Vec::new(); @@ -37,80 +52,121 @@ impl RemotePackageDB { manifests.push(manifest); } manifests.push(Manifest::from_config(config.server().clone(), config).await?); - Ok(Self(manifests)) + Ok(Self(Impl::LuarocksManifests(manifests))) } - /// Find a package that matches the requirement + /// Find a remote package that matches the requirement, returning the latest match. pub(crate) fn find( &self, package_req: &PackageReq, filter: Option, progress: &Progress, ) -> Result { - let result = self.0.iter().find_map(|manifest| { - progress.map(|p| p.set_message(format!("🔎 Searching {}", &manifest.server_url()))); - manifest.search(package_req, filter.clone()) - }); - match result { - Some(package) => Ok(package), - None => Err(SearchError::RockNotFound(package_req.clone())), + match &self.0 { + Impl::LuarocksManifests(manifests) => match manifests.iter().find_map(|manifest| { + progress.map(|p| p.set_message(format!("🔎 Searching {}", &manifest.server_url()))); + manifest.find(package_req, filter.clone()) + }) { + Some(package) => Ok(package), + None => Err(SearchError::RockNotFound(package_req.clone())), + }, + Impl::Lockfile(lockfile) => { + match lockfile.has_rock(package_req, filter).map(|local_package| { + RemotePackage::new( + PackageSpec::new(local_package.spec.name, local_package.spec.version), + local_package.source, + ) + }) { + Some(package) => Ok(package), + None => Err(SearchError::RockNotFoundInLockfile(package_req.clone())), + } + } } } - /// Search for all packages that match the requirement + /// Search for all packages that match the requirement. pub fn search(&self, package_req: &PackageReq) -> Vec<(&PackageName, Vec<&PackageVersion>)> { - self.0 - .iter() - .flat_map(|manifest| { - manifest - .metadata() - .repository - .iter() - .filter_map(|(name, elements)| { - if name.to_string().contains(&package_req.name().to_string()) { - Some(( - name, - elements - .keys() - .filter(|version| package_req.version_req().matches(version)) - .sorted_by(|a, b| Ord::cmp(b, a)) - .collect_vec(), - )) - } else { - None - } - }) - }) - .collect() + match &self.0 { + Impl::LuarocksManifests(manifests) => manifests + .iter() + .flat_map(|manifest| { + manifest + .metadata() + .repository + .iter() + .filter_map(|(name, elements)| { + if name.to_string().contains(&package_req.name().to_string()) { + Some(( + name, + elements + .keys() + .filter(|version| { + package_req.version_req().matches(version) + }) + .sorted_by(|a, b| Ord::cmp(b, a)) + .collect_vec(), + )) + } else { + None + } + }) + }) + .collect(), + Impl::Lockfile(lockfile) => lockfile + .rocks() + .iter() + .filter_map(|(_, package)| { + // NOTE: This doesn't group packages by name, but we don't care for now, + // as we shouldn't need to use this function with a lockfile. + let name = package.name(); + if name.to_string().contains(&package_req.name().to_string()) { + Some((name, vec![package.version()])) + } else { + None + } + }) + .collect_vec(), + } } - pub fn latest_version(&self, rock_name: &PackageName) -> Option<&PackageVersion> { - self.0 - .iter() - .filter_map(|manifest| manifest.metadata().latest_version(rock_name)) - .sorted() - .last() + /// Find the latest version for a package by name. + pub(crate) fn latest_version(&self, rock_name: &PackageName) -> Option { + self.latest_match(&rock_name.clone().into(), None) + .map(|result| result.version().clone()) } + /// Find the latest package that matches the requirement. pub fn latest_match( &self, package_req: &PackageReq, filter: Option, ) -> Option { - self.0 - .iter() - .filter_map(|manifest| { - manifest - .metadata() - .latest_match(package_req, filter.clone()) - .map(|x| x.0) - }) - .last() + match self.find(package_req, filter, &Progress::NoProgress) { + Ok(result) => Some(result.package), + Err(_) => None, + } + } + + /// Validate the integrity of an installed package. + pub(crate) fn validate_integrity( + &self, + package: &LocalPackage, + ) -> Result<(), RemotePackageDbIntegrityError> { + match &self.0 { + Impl::LuarocksManifests(_) => Ok(()), + Impl::Lockfile(lockfile) => Ok(lockfile.validate_integrity(package)?), + } } } impl From for RemotePackageDB { fn from(manifest: Manifest) -> Self { - RemotePackageDB(vec![manifest]) + Self(Impl::LuarocksManifests(vec![manifest])) + } +} + +impl From for RemotePackageDB { + fn from(lockfile: Lockfile) -> Self { + Self(Impl::Lockfile(lockfile)) } } diff --git a/rocks-lib/src/rockspec/platform.rs b/rocks-lib/src/rockspec/platform.rs index 120bb287..22f2b78f 100644 --- a/rocks-lib/src/rockspec/platform.rs +++ b/rocks-lib/src/rockspec/platform.rs @@ -296,9 +296,17 @@ impl PerPlatform { ) } + fn set(&mut self, platform: PlatformIdentifier, value: T) { + self.per_platform.insert(platform, value); + } + pub fn current_platform(&self) -> &T { self.get(&get_platform()) } + + pub(crate) fn current_platform_set(&mut self, value: T) { + self.set(get_platform(), value) + } } impl Default for PerPlatform { diff --git a/rocks-lib/src/tree/mod.rs b/rocks-lib/src/tree/mod.rs index 18ddfaeb..95255814 100644 --- a/rocks-lib/src/tree/mod.rs +++ b/rocks-lib/src/tree/mod.rs @@ -208,8 +208,13 @@ impl Tree { Ok(rock_layout) } + /// Get this tree's lockfile path. + pub fn lockfile_path(&self) -> PathBuf { + self.root().join("lock.json") + } + pub fn lockfile(&self) -> io::Result { - Lockfile::new(self.root().join("lock.json")) + Lockfile::new(self.lockfile_path()) } }