feat: improve installing from repositories

Signed-off-by: sisungo <[email protected]>
This commit is contained in:
2026-06-25 05:41:09 +08:00
parent e2ddb7b13b
commit 1c8aba9d56
10 changed files with 243 additions and 199 deletions
+1
View File
@@ -10,6 +10,7 @@ path = "src/main.rs"
[dependencies]
anyhow = "1"
clap = { version = "4", features = ["derive"] }
console = "0.16"
packie = { path = "../" }
rust-i18n = "4"
indicatif = "0.18"
+41
View File
@@ -1 +1,42 @@
use console::Term;
pub fn yesno(prompt: &str) -> bool {
eprint!("{prompt} [y/N] ");
["y", "Y"].contains(
&std::io::stdin()
.lines()
.next()
.transpose()
.unwrap_or_default()
.unwrap_or_default()
.trim(),
)
}
pub fn print_list(indent: u32, list: impl Iterator<Item = impl AsRef<str>>) {
let (_, column) = Term::stdout().size();
let mut line_used: usize = 0;
for i in list {
let mut i = i.as_ref().to_string();
if line_used >= column as _ {
line_used = 0;
println!();
}
if i.contains(' ') {
i = format!("'{i}'");
}
let to_print = format!("{}{}", " ".repeat(indent as _), i);
print!("{to_print}");
line_used += to_print.len();
}
println!();
}
pub fn format_size(bytes: u64) -> String {
match bytes {
..1000 => format!("{bytes} B"),
..1_000_000 => format!("{} kB", bytes / 1000),
..1_000_000_000 => format!("{:.2} MB", (bytes as f64) / 1000000.),
_ => format!("{:.3} GB", (bytes as f64) / 1000000000.),
}
}
+3 -1
View File
@@ -2,6 +2,8 @@ use anyhow::anyhow;
use clap::Parser;
use packie::{PackieBuilder, package::PkgSpec};
use crate::common::format_size;
#[derive(Debug, Parser)]
pub struct Cli {
pkgspec: PkgSpec,
@@ -22,7 +24,7 @@ pub fn main(cli: Cli) -> anyhow::Result<()> {
println!("Package version: {}", found.pkg_manifest.version);
println!("Package architecture: {}", found.pkg_manifest.arch);
println!("Install package spec: {}", found.install_pkgspec);
println!("Installed size: {}", found.installed_size);
println!("Installed size: {}", format_size(found.installed_size));
Ok(())
}
+72 -50
View File
@@ -1,67 +1,89 @@
use crate::common::{format_size, print_list, yesno};
use clap::Parser;
use indicatif::ProgressBar;
use packie::{
PackieBuilder,
install::{InstallFromRepoEvent, InstallOptions},
package::PkgSpec,
};
use std::{
path::{Path, PathBuf},
str::FromStr,
PackieBuilder, install::InstallOptions, package::PkgSpec, repo::DownloadPackageEvent,
};
use rust_i18n::t;
#[derive(Debug, Parser)]
pub struct Cli {
items: Vec<Item>,
}
#[arg(short, long)]
file: bool,
#[derive(Debug, Clone)]
enum Item {
LocalFile(PathBuf),
PkgSpec(PkgSpec),
}
impl FromStr for Item {
type Err = anyhow::Error;
#[arg(short, long)]
yes: bool,
fn from_str(s: &str) -> Result<Self, Self::Err> {
if Path::new(s).exists() {
Ok(Self::LocalFile(s.into()))
} else {
Ok(Self::PkgSpec(s.parse()?))
}
}
items: Vec<String>,
}
pub fn main(cli: Cli) -> anyhow::Result<()> {
let mut packie = PackieBuilder::new().build()?;
for i in cli.items {
match i {
Item::LocalFile(path) => {
packie.install_package_file(
path,
&InstallOptions {
install_pkgspec: None,
},
)?;
}
Item::PkgSpec(pkgspec) => {
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()?;
}
if cli.file {
for item in cli.items {
packie.install_package_file(item, &InstallOptions::default())?;
}
return Ok(());
}
let mut packages = Vec::with_capacity(cli.items.len() * 4);
for i in cli.items {
packages.append(&mut packie.calculate_depgraph(i.parse::<PkgSpec>()?)?);
}
packages.dedup_by_key(|x| x.repo_pkg.pkg_manifest.pkg_ident());
if packages.is_empty() {
eprintln!("No packages to install.");
return Ok(());
}
let download_size: u64 = packages.iter().map(|x| x.repo_pkg.download_size).sum();
let installed_size: u64 = packages.iter().map(|x| x.repo_pkg.installed_size).sum();
println!("{}", t!("install.prompt_to_install"));
print_list(4, packages.iter().map(|x| x.pkgspec.to_string()));
println!(
"{}",
t!("install.download_size", size = format_size(download_size))
);
println!(
"{}",
t!("install.installed_size", size = format_size(installed_size))
);
if cli.yes {
eprintln!("y");
} else if !yesno(&t!("install.confirm_installation")) {
eprintln!("{}", t!("install.aborting"));
return Ok(());
}
let progress_bar = ProgressBar::new(download_size);
let mut package_files = Vec::with_capacity(packages.len());
for package in packages.iter() {
let init_pos = progress_bar.position();
progress_bar.println(format!("Downloading \"{}\"...", package.pkgspec));
let path = packie.download_package(
&package.repo_name,
&package.repo_pkg.pkg_manifest.pkg_ident(),
|ev| match ev {
DownloadPackageEvent::Progress(_, sum, _) => {
progress_bar.set_position(init_pos + sum);
}
DownloadPackageEvent::Error(url, err) => {
progress_bar
.println(format!("error: failed to download from \"{url}\": {err}"));
}
},
)?;
package_files.push(path);
}
let progress_bar = ProgressBar::new(package_files.len() as _);
for package in package_files.iter() {
progress_bar.println(format!("Installing \"{}\"...", package.display()));
packie.install_package_file(package, &InstallOptions::default())?;
progress_bar.inc(1);
}
Ok(())
}
+5
View File
@@ -5,6 +5,7 @@ mod install;
mod print;
mod remove;
mod repo_admin;
mod update;
use clap::Parser;
use rust_i18n::t;
@@ -40,6 +41,9 @@ enum Subcommand {
/// Administrate repository service
RepoAdmin(repo_admin::Cli),
/// Update repository indexes
Update(update::Cli),
}
fn main() {
@@ -62,6 +66,7 @@ fn main() {
Subcommand::Print(cli) => print::main(cli),
Subcommand::Remove(cli) => remove::main(cli),
Subcommand::RepoAdmin(cli) => repo_admin::main(cli),
Subcommand::Update(cli) => update::main(cli),
};
if let Err(err) = result {
eprintln!("{}", t!("main.error", error = err));
+24
View File
@@ -0,0 +1,24 @@
use clap::Parser;
use indicatif::ProgressBar;
use packie::{PackieBuilder, repo::SyncEvent};
#[derive(Debug, Parser)]
pub struct Cli {}
pub fn main(cli: Cli) -> anyhow::Result<()> {
let mut packie = PackieBuilder::new().build()?;
let progress_bar = ProgressBar::new(0);
packie
.sync_repo()
.on_event(|ev| match ev {
SyncEvent::Progress(progress) => {
progress_bar.set_length(progress.total_bytes);
progress_bar.set_position(progress.downloaded_bytes);
}
SyncEvent::Error(repo, url, err) => {
eprintln!("failed to sync repository: {err}")
}
})
.run();
Ok(())
}
+17 -2
View File
@@ -1,4 +1,19 @@
_version: 2
main.error:
en: 'error: %{error}'
zh-CN: '错误:%{error}'
en: "error: %{error}"
zh-CN: "错误:%{error}"
install.prompt_to_install:
en: "The following packages are to be installed:"
zh-CN: "将要安装如下的包:"
install.download_size:
en: "Download size: %{size}"
zh-CN: "下载体积:%{size}"
install.installed_size:
en: "Installed size: %{size}"
zh-CN: "安装体积:%{size}"
install.confirm_installation:
en: "Confirm to install?"
zh-CN: "确认安装?"
install.aborting:
en: "Aborting."
zh-CN: "中止。"
+59 -143
View File
@@ -1,8 +1,7 @@
use crate::{
Packie,
local::Installation,
package::{Package, PackageError, PkgIdent, PkgSpec},
repo::{DownloadPackageEvent, RepoError, RepoPackage},
repo::{RepoError, RepoPackage},
};
use rust_i18n::t;
use std::path::Path;
@@ -18,8 +17,58 @@ impl InstallOptions {
}
impl super::Packie {
pub fn install_from_repo(&mut self, pkgspec: PkgSpec) -> InstallFromRepoSession<'_> {
InstallFromRepoSession::new(self, pkgspec)
pub fn calculate_depgraph(
&mut self,
pkgspec: PkgSpec,
) -> Result<Vec<DepgraphNode>, InstallError> {
let prior_arch = self.profile.host_arch.clone();
let mut query = self.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(|(_, (_, p))| p.pkg_manifest.pkg_ident() == prior_pkg)
.unwrap()
.0;
let (repo_name, repo_pkg) = found.remove(prior_pkg);
if !self
.search_installation(&pkgspec)
.unwrap_or_default()
.is_empty()
{
continue;
}
depgraph.push(DepgraphNode {
repo_name,
pkgspec,
repo_pkg: repo_pkg.clone(),
});
for dep in &repo_pkg.pkg_manifest.dependencies {
stack.push(dep.clone());
}
}
depgraph.reverse();
Ok(depgraph)
}
/// Installs a package, from a local package file.
@@ -91,88 +140,9 @@ impl super::Packie {
}
}
#[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,
}
}
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))]
#[error("{}", t!("install.InstallError.NotFound", item = .0))]
NotFound(String),
#[error("{}", t!("install.InstallError.DependencyRing"))]
@@ -193,7 +163,7 @@ pub enum InstallError {
#[error("{0}")]
Repo(RepoError),
#[error("{}", t!("install.InstallError.Database", error = 0))]
#[error("{}", t!("install.InstallError.Database", error = .0))]
Database(rusqlite::Error),
}
impl From<PackageError> for InstallError {
@@ -211,64 +181,10 @@ fn default_install_pkgspec(pkg_ident: PkgIdent) -> PkgSpec {
}
#[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)
pub struct DepgraphNode {
pub repo_name: String,
pub pkgspec: PkgSpec,
pub repo_pkg: RepoPackage,
}
/// Gets prior package that is installed by default in package candidates.
+19 -1
View File
@@ -114,7 +114,7 @@ impl PkgManifest {
}
/// A package specifier.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone)]
pub struct PkgSpec {
pub name: String,
pub version: VersionFilter,
@@ -205,6 +205,24 @@ impl Display for PkgSpec {
Ok(())
}
}
impl Serialize for PkgSpec {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.to_string().serialize(serializer)
}
}
impl<'de> Deserialize<'de> for PkgSpec {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
String::deserialize(deserializer)?
.parse::<Self>()
.map_err(|e| serde::de::Error::custom(e))
}
}
/// A package identifier that specifies a unique package.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+2 -2
View File
@@ -45,9 +45,9 @@ pub enum RemoveError {
#[error("{}", t!("remove.RemoveError.NotUniquePkgSpec"))]
NotUniquePkgSpec,
#[error("{}", t!("remove.RemoveError.RemoveFiles", error = 0))]
#[error("{}", t!("remove.RemoveError.RemoveFiles", error = .0))]
RemoveFiles(std::io::Error),
#[error("{}", t!("remove.RemoveError.Database", error = 0))]
#[error("{}", t!("remove.RemoveError.Database", error = .0))]
Database(rusqlite::Error),
}