forked from semios/packie
feat: improve installing from repositories
Signed-off-by: sisungo <[email protected]>
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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));
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user