From 1c8aba9d56a1bbd409c79e4b441e232a4abf0815 Mon Sep 17 00:00:00 2001 From: sisungo Date: Thu, 25 Jun 2026 05:41:09 +0800 Subject: [PATCH] feat: improve installing from repositories Signed-off-by: sisungo --- cli/Cargo.toml | 1 + cli/src/common.rs | 41 ++++++++ cli/src/info.rs | 4 +- cli/src/install.rs | 122 +++++++++++++--------- cli/src/main.rs | 5 + cli/src/update.rs | 24 +++++ locales/packie-cli/main.yml | 19 +++- src/install.rs | 202 +++++++++++------------------------- src/package.rs | 20 +++- src/remove.rs | 4 +- 10 files changed, 243 insertions(+), 199 deletions(-) create mode 100644 cli/src/update.rs diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 1a9d614..996e5e9 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -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" diff --git a/cli/src/common.rs b/cli/src/common.rs index 8b13789..3c6cb3a 100644 --- a/cli/src/common.rs +++ b/cli/src/common.rs @@ -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>) { + 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.), + } +} diff --git a/cli/src/info.rs b/cli/src/info.rs index 8f1569c..e75f7aa 100644 --- a/cli/src/info.rs +++ b/cli/src/info.rs @@ -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(()) } diff --git a/cli/src/install.rs b/cli/src/install.rs index 9f0373a..7df85a5 100644 --- a/cli/src/install.rs +++ b/cli/src/install.rs @@ -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, -} + #[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 { - if Path::new(s).exists() { - Ok(Self::LocalFile(s.into())) - } else { - Ok(Self::PkgSpec(s.parse()?)) - } - } + items: Vec, } 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::()?)?); + } + 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(()) } diff --git a/cli/src/main.rs b/cli/src/main.rs index e915362..95781cf 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -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)); diff --git a/cli/src/update.rs b/cli/src/update.rs new file mode 100644 index 0000000..4da6279 --- /dev/null +++ b/cli/src/update.rs @@ -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(()) +} diff --git a/locales/packie-cli/main.yml b/locales/packie-cli/main.yml index f4daf21..1280a91 100644 --- a/locales/packie-cli/main.yml +++ b/locales/packie-cli/main.yml @@ -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: "中止。" diff --git a/src/install.rs b/src/install.rs index 4a76b21..e23c011 100644 --- a/src/install.rs +++ b/src/install.rs @@ -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, 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(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 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, 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. diff --git a/src/package.rs b/src/package.rs index 64e434c..507ed5a 100644 --- a/src/package.rs +++ b/src/package.rs @@ -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(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.to_string().serialize(serializer) + } +} +impl<'de> Deserialize<'de> for PkgSpec { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + String::deserialize(deserializer)? + .parse::() + .map_err(|e| serde::de::Error::custom(e)) + } +} /// A package identifier that specifies a unique package. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] diff --git a/src/remove.rs b/src/remove.rs index 22b636b..6a975da 100644 --- a/src/remove.rs +++ b/src/remove.rs @@ -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), }