feat: add installing from repositories
Signed-off-by: sisungo <[email protected]>
This commit is contained in:
+22
-4
@@ -1,5 +1,10 @@
|
||||
use clap::Parser;
|
||||
use packie::{PackieBuilder, install::InstallOptions, package::PkgSpec};
|
||||
use indicatif::ProgressBar;
|
||||
use packie::{
|
||||
PackieBuilder,
|
||||
install::{InstallFromRepoEvent, InstallOptions},
|
||||
package::PkgSpec,
|
||||
};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
@@ -32,8 +37,7 @@ pub fn main(cli: Cli) -> anyhow::Result<()> {
|
||||
for i in cli.items {
|
||||
match i {
|
||||
Item::LocalFile(path) => {
|
||||
packie::install::install_package_file(
|
||||
&mut packie,
|
||||
packie.install_package_file(
|
||||
path,
|
||||
&InstallOptions {
|
||||
install_pkgspec: None,
|
||||
@@ -41,7 +45,21 @@ pub fn main(cli: Cli) -> anyhow::Result<()> {
|
||||
)?;
|
||||
}
|
||||
Item::PkgSpec(pkgspec) => {
|
||||
todo!()
|
||||
let progress = ProgressBar::new(0);
|
||||
packie
|
||||
.install_from_repo(pkgspec)
|
||||
.on_event(|ev| match ev {
|
||||
InstallFromRepoEvent::Download(ev, sum, total) => {
|
||||
progress.set_length(total);
|
||||
progress.set_position(sum);
|
||||
}
|
||||
InstallFromRepoEvent::Install(pkg, sum, total) => {
|
||||
progress.set_prefix(pkg);
|
||||
progress.set_length(total);
|
||||
progress.set_position(sum);
|
||||
}
|
||||
})
|
||||
.run()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,12 @@ common.CopyError:
|
||||
install.InstallError.Database:
|
||||
en: "local database error: %{error}"
|
||||
zh-CN: "本地数据库错误:%{error}"
|
||||
install.InstallError.NotFound:
|
||||
en: "item not found: %{item}"
|
||||
zh-CN: "找不到项目:%{item}"
|
||||
install.InstallError.DependencyRing:
|
||||
en: "dependency ring detected"
|
||||
zh-CN: "检测到依赖环"
|
||||
remove.RemoveError.NotUniquePkgSpec:
|
||||
en: "not unique package spec"
|
||||
zh-CN: "指定的包范围匹配到了多个包"
|
||||
|
||||
+231
-58
@@ -2,86 +2,182 @@ use crate::{
|
||||
Packie,
|
||||
local::Installation,
|
||||
package::{Package, PackageError, PkgIdent, PkgSpec},
|
||||
repo::{DownloadPackageEvent, RepoError, RepoPackage},
|
||||
};
|
||||
use rust_i18n::t;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct InstallOptions {
|
||||
pub install_pkgspec: Option<PkgSpec>,
|
||||
}
|
||||
|
||||
/// Installs a package, from a local package file.
|
||||
pub fn install_package_file<P: AsRef<Path>>(
|
||||
packie: &mut Packie,
|
||||
path: P,
|
||||
options: &InstallOptions,
|
||||
) -> Result<(), InstallError> {
|
||||
let package = Package::open(path)?;
|
||||
install_package(packie, &package, options)
|
||||
impl InstallOptions {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Installs a package, from a constructed [`Package`].
|
||||
pub fn install_package(
|
||||
packie: &mut Packie,
|
||||
package: &Package,
|
||||
options: &InstallOptions,
|
||||
) -> Result<(), InstallError> {
|
||||
// Collect necessary information
|
||||
let pkg_manifest = package.manifest()?;
|
||||
let installed_size = package.installed_size()?;
|
||||
let install_pkgspec = options
|
||||
.install_pkgspec
|
||||
.clone()
|
||||
.unwrap_or_else(|| default_install_pkgspec(pkg_manifest.pkg_ident()));
|
||||
|
||||
// Check if the package is previously installed
|
||||
if !packie
|
||||
.search_installation(&pkg_manifest.pkg_ident().into())
|
||||
.map_err(InstallError::Database)?
|
||||
.is_empty()
|
||||
{
|
||||
return Err(InstallError::AlreadyInstalled);
|
||||
impl super::Packie {
|
||||
pub fn install_from_repo(&mut self, pkgspec: PkgSpec) -> InstallFromRepoSession<'_> {
|
||||
InstallFromRepoSession::new(self, pkgspec)
|
||||
}
|
||||
|
||||
// Check if the package spec is valid
|
||||
if !install_pkgspec.matches(&pkg_manifest.pkg_ident()) {
|
||||
return Err(InstallError::PkgSpec);
|
||||
/// Installs a package, from a local package file.
|
||||
pub fn install_package_file<P: AsRef<Path>>(
|
||||
&mut self,
|
||||
path: P,
|
||||
options: &InstallOptions,
|
||||
) -> Result<(), InstallError> {
|
||||
let package = Package::open(path)?;
|
||||
self.install_package(&package, options)
|
||||
}
|
||||
|
||||
// Copy package files
|
||||
let dest_dir = packie
|
||||
.profile
|
||||
.pkg_dir
|
||||
.join(pkg_manifest.pkg_ident().to_string());
|
||||
crate::common::copy_dir(package.bundle_dir(), dest_dir).map_err(InstallError::Copy)?;
|
||||
/// Installs a package, from a constructed [`Package`].
|
||||
pub fn install_package(
|
||||
&mut self,
|
||||
package: &Package,
|
||||
options: &InstallOptions,
|
||||
) -> Result<(), InstallError> {
|
||||
// Collect necessary information
|
||||
let pkg_manifest = package.manifest()?;
|
||||
let installed_size = package.installed_size()?;
|
||||
let install_pkgspec = options
|
||||
.install_pkgspec
|
||||
.clone()
|
||||
.unwrap_or_else(|| default_install_pkgspec(pkg_manifest.pkg_ident()));
|
||||
|
||||
// Record installation in the database
|
||||
let installation = Installation {
|
||||
pkg_manifest: pkg_manifest.clone(),
|
||||
install_pkgspec,
|
||||
install_date: crate::common::timestamp_secs(),
|
||||
update_date: crate::common::timestamp_secs(),
|
||||
installed_size,
|
||||
};
|
||||
packie
|
||||
.local_db
|
||||
.insert_installation(&installation)
|
||||
.map_err(InstallError::Database)?;
|
||||
// Check if the package is previously installed
|
||||
if !self
|
||||
.search_installation(&pkg_manifest.pkg_ident().into())
|
||||
.map_err(InstallError::Database)?
|
||||
.is_empty()
|
||||
{
|
||||
return Err(InstallError::AlreadyInstalled);
|
||||
}
|
||||
|
||||
// Enable default links
|
||||
for link in pkg_manifest.links.iter() {
|
||||
if !link.default {
|
||||
continue;
|
||||
// Check if the package spec is valid
|
||||
if !install_pkgspec.matches(&pkg_manifest.pkg_ident()) {
|
||||
return Err(InstallError::PkgSpec);
|
||||
}
|
||||
|
||||
// Copy package files
|
||||
let dest_dir = self
|
||||
.profile
|
||||
.pkg_dir
|
||||
.join(pkg_manifest.pkg_ident().to_string());
|
||||
crate::common::copy_dir(package.bundle_dir(), dest_dir).map_err(InstallError::Copy)?;
|
||||
|
||||
// Record installation in the database
|
||||
let installation = Installation {
|
||||
pkg_manifest: pkg_manifest.clone(),
|
||||
install_pkgspec,
|
||||
install_date: crate::common::timestamp_secs(),
|
||||
update_date: crate::common::timestamp_secs(),
|
||||
installed_size,
|
||||
};
|
||||
self.local_db
|
||||
.insert_installation(&installation)
|
||||
.map_err(InstallError::Database)?;
|
||||
|
||||
// Enable default links
|
||||
for link in pkg_manifest.links.iter() {
|
||||
if !link.default {
|
||||
continue;
|
||||
}
|
||||
_ = crate::link::activate_by_description(self, &pkg_manifest.pkg_ident(), link);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InstallFromRepoSession<'a, E = fn(InstallFromRepoEvent)> {
|
||||
packie: &'a mut Packie,
|
||||
pkgspec: PkgSpec,
|
||||
options: InstallOptions,
|
||||
on_event: E,
|
||||
}
|
||||
impl<'a> InstallFromRepoSession<'a, fn(InstallFromRepoEvent)> {
|
||||
pub fn new(packie: &'a mut Packie, pkgspec: PkgSpec) -> Self {
|
||||
Self {
|
||||
packie,
|
||||
pkgspec,
|
||||
options: InstallOptions::default(),
|
||||
on_event: |_| (),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'a, E> InstallFromRepoSession<'a, E> {
|
||||
pub fn on_event<E1>(self, on_event: E1) -> InstallFromRepoSession<'a, E1> {
|
||||
InstallFromRepoSession {
|
||||
packie: self.packie,
|
||||
pkgspec: self.pkgspec,
|
||||
options: self.options,
|
||||
on_event,
|
||||
}
|
||||
_ = crate::link::activate_by_description(packie, &pkg_manifest.pkg_ident(), link);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
pub fn options(mut self, options: InstallOptions) -> Self {
|
||||
self.options = options;
|
||||
self
|
||||
}
|
||||
}
|
||||
impl<'a, E: FnMut(InstallFromRepoEvent)> InstallFromRepoSession<'a, E> {
|
||||
pub fn run(mut self) -> Result<(), InstallError> {
|
||||
let depgraph = calculate_depgraph(self.packie, self.pkgspec)?;
|
||||
if depgraph.is_empty() {
|
||||
return Err(InstallError::AlreadyInstalled);
|
||||
}
|
||||
|
||||
// 1. Download all packages
|
||||
let mut packages = Vec::with_capacity(depgraph.len());
|
||||
for (count, node) in depgraph.iter().enumerate() {
|
||||
let path = self
|
||||
.packie
|
||||
.download_package(
|
||||
&node.repo_name,
|
||||
&node.repo_pkg.pkg_manifest.pkg_ident(),
|
||||
|ev| {
|
||||
(self.on_event)(InstallFromRepoEvent::Download(
|
||||
ev,
|
||||
count as _,
|
||||
depgraph.len() as _,
|
||||
))
|
||||
},
|
||||
)
|
||||
.map_err(InstallError::Repo)?;
|
||||
packages.push(path);
|
||||
}
|
||||
|
||||
// 2. Install all packages
|
||||
for (count, pkg) in packages.iter().enumerate() {
|
||||
self.packie.install_package_file(&pkg, &self.options)?;
|
||||
(self.on_event)(InstallFromRepoEvent::Install(
|
||||
pkg.display().to_string(),
|
||||
count as _,
|
||||
packages.len() as _,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum InstallFromRepoEvent {
|
||||
Download(DownloadPackageEvent, u64, u64),
|
||||
Install(String, u64, u64),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum InstallError {
|
||||
#[error("{}", t!("install.InstallError.NotFound", item = 0))]
|
||||
NotFound(String),
|
||||
|
||||
#[error("{}", t!("install.InstallError.DependencyRing"))]
|
||||
DependencyRing,
|
||||
|
||||
#[error("{0}")]
|
||||
Package(PackageError),
|
||||
|
||||
@@ -94,6 +190,9 @@ pub enum InstallError {
|
||||
#[error("{}", t!("install.InstallError.AlreadyInstalled"))]
|
||||
AlreadyInstalled,
|
||||
|
||||
#[error("{0}")]
|
||||
Repo(RepoError),
|
||||
|
||||
#[error("{}", t!("install.InstallError.Database", error = 0))]
|
||||
Database(rusqlite::Error),
|
||||
}
|
||||
@@ -110,3 +209,77 @@ fn default_install_pkgspec(pkg_ident: PkgIdent) -> PkgSpec {
|
||||
arch: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct DepgraphNode {
|
||||
repo_name: String,
|
||||
pkgspec: PkgSpec,
|
||||
repo_pkg: RepoPackage,
|
||||
}
|
||||
|
||||
fn calculate_depgraph(
|
||||
packie: &mut Packie,
|
||||
pkgspec: PkgSpec,
|
||||
) -> Result<Vec<DepgraphNode>, InstallError> {
|
||||
let prior_arch = packie.profile.host_arch.clone();
|
||||
|
||||
let mut query = packie.query_repo();
|
||||
|
||||
let mut depgraph = Vec::new();
|
||||
let mut stack = vec![pkgspec];
|
||||
|
||||
loop {
|
||||
let Some(pkgspec) = stack.pop() else {
|
||||
break;
|
||||
};
|
||||
let mut found = query.search_package(&pkgspec);
|
||||
if found.is_empty() {
|
||||
return Err(InstallError::NotFound(pkgspec.to_string()));
|
||||
}
|
||||
let prior_pkg = prior_pkg(
|
||||
found.iter().map(|(_, x)| x.pkg_manifest.pkg_ident()),
|
||||
&prior_arch,
|
||||
);
|
||||
let prior_pkg = found
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(n, (_, p))| p.pkg_manifest.pkg_ident() == prior_pkg)
|
||||
.unwrap()
|
||||
.0;
|
||||
let (repo_name, repo_pkg) = found.remove(prior_pkg);
|
||||
|
||||
depgraph.push(DepgraphNode {
|
||||
repo_name,
|
||||
pkgspec,
|
||||
repo_pkg: repo_pkg.clone(),
|
||||
});
|
||||
|
||||
for dep in &repo_pkg.pkg_manifest.dependencies {
|
||||
if !packie
|
||||
.search_installation(&dep)
|
||||
.unwrap_or_default()
|
||||
.is_empty()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
stack.push(dep.clone());
|
||||
}
|
||||
}
|
||||
|
||||
depgraph.reverse();
|
||||
Ok(depgraph)
|
||||
}
|
||||
|
||||
/// Gets prior package that is installed by default in package candidates.
|
||||
fn prior_pkg(all: impl Iterator<Item = PkgIdent>, prior_arch: &str) -> PkgIdent {
|
||||
let mut all = all.collect::<Vec<_>>();
|
||||
all.sort_by(|x, y| x.version.cmp(&y.version));
|
||||
all.reverse();
|
||||
for i in &all {
|
||||
if i.arch == prior_arch {
|
||||
return i.clone();
|
||||
}
|
||||
}
|
||||
all.remove(0)
|
||||
}
|
||||
|
||||
+1
-1
@@ -207,7 +207,7 @@ impl Display for PkgSpec {
|
||||
}
|
||||
|
||||
/// A package identifier that specifies a unique package.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct PkgIdent {
|
||||
pub name: String,
|
||||
pub version: Version,
|
||||
|
||||
+40
-1
@@ -4,12 +4,13 @@ mod serve;
|
||||
#[doc(inline)]
|
||||
pub use download::DownloadError;
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
#[doc(inline)]
|
||||
pub use serve::RepoServeDir;
|
||||
|
||||
use crate::{
|
||||
Packie,
|
||||
package::{PkgIdent, PkgManifest},
|
||||
package::{PkgIdent, PkgManifest, PkgSpec},
|
||||
};
|
||||
use download::Download;
|
||||
use rusqlite::{OpenFlags, Row, params};
|
||||
@@ -24,6 +25,11 @@ impl super::Packie {
|
||||
SyncSession::new(self)
|
||||
}
|
||||
|
||||
/// Create a session for querying repositories.
|
||||
pub fn query_repo(&mut self) -> QuerySession {
|
||||
QuerySession::new(self)
|
||||
}
|
||||
|
||||
/// Download a package.
|
||||
pub fn download_package(
|
||||
&self,
|
||||
@@ -309,6 +315,39 @@ pub enum DownloadPackageEvent {
|
||||
Error(Url, RepoError),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct QuerySession {
|
||||
repos: FxHashMap<String, RepoDb>,
|
||||
}
|
||||
impl QuerySession {
|
||||
pub fn new(packie: &mut Packie) -> Self {
|
||||
let mut repos = FxHashMap::default();
|
||||
for name in packie.config.repos.keys() {
|
||||
let repo_db = packie.cache.repo_dir(name).join(RepoDb::FILENAME);
|
||||
let Ok(repo_db) = RepoDb::open_ro(repo_db) else {
|
||||
continue;
|
||||
};
|
||||
repos.insert(name.into(), repo_db);
|
||||
}
|
||||
Self { repos }
|
||||
}
|
||||
|
||||
pub fn search_package(&mut self, pkgspec: &PkgSpec) -> Vec<(String, RepoPackage)> {
|
||||
let mut all = Vec::new();
|
||||
for (name, db) in self.repos.iter_mut() {
|
||||
for pkg in db
|
||||
.select_package_by_pkgname(&pkgspec.name)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
if pkgspec.matches(&pkg.pkg_manifest.pkg_ident()) {
|
||||
all.push((name.into(), pkg));
|
||||
}
|
||||
}
|
||||
}
|
||||
all
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum RepoError {
|
||||
#[error("{}", t!("repo.RepoError.NoSuchRepo"))]
|
||||
|
||||
@@ -95,6 +95,11 @@ impl PartialOrd for Version {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Ord for Version {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.partial_cmp(other).unwrap()
|
||||
}
|
||||
}
|
||||
impl Display for Version {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.namespace != 0 {
|
||||
|
||||
Reference in New Issue
Block a user