feat: add installing from repositories

Signed-off-by: sisungo <[email protected]>
This commit is contained in:
2026-06-24 18:44:12 +08:00
parent b6dbfd12be
commit e2ddb7b13b
6 changed files with 305 additions and 64 deletions
+22 -4
View File
@@ -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()?;
}
}
}
+6
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"))]
+5
View File
@@ -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 {