From ac3593c2d534f77277b8218ee7b10c226af499d3 Mon Sep 17 00:00:00 2001 From: sisungo Date: Mon, 17 Aug 2026 18:20:33 +0800 Subject: [PATCH] feat: introduce more versioning options Signed-off-by: sisungo --- cli/src/common.rs | 25 +++++++++-------- cli/src/info.rs | 23 +++++++++++----- cli/src/install.rs | 2 +- cli/src/remove.rs | 25 ++++++++++++++--- locales/libpackie/main.yml | 10 +++++++ locales/packie-cli/main.yml | 22 ++++++++++----- src/common/fs.rs | 9 +++++++ src/package.rs | 35 +++++++++++++++++++++--- src/version.rs | 54 ++++++++++++++++++++----------------- 9 files changed, 149 insertions(+), 56 deletions(-) diff --git a/cli/src/common.rs b/cli/src/common.rs index 7f0eadc..09bff07 100644 --- a/cli/src/common.rs +++ b/cli/src/common.rs @@ -28,17 +28,20 @@ pub fn progress_style_multidownload() -> ProgressStyle { ProgressStyle::with_template(&lines.join("\n")).unwrap() } -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 yesno(prompt: &str, default: bool) -> bool { + if default { + eprint!("{prompt} [Y/n] "); + } else { + eprint!("{prompt} [y/N] "); + } + let line = std::io::stdin() + .lines() + .next() + .transpose() + .unwrap_or_default() + .unwrap_or_default(); + let line = line.trim(); + ["y", "Y"].contains(&line) || (line.is_empty() && default) } pub fn print_list(indent: u32, list: impl Iterator>) { diff --git a/cli/src/info.rs b/cli/src/info.rs index 869acaf..45bd179 100644 --- a/cli/src/info.rs +++ b/cli/src/info.rs @@ -25,6 +25,7 @@ pub fn main(cli: Cli) -> Result<(), crate::Error> { for i in found.iter() { writeln!(&mut note, " - {}", i.pkg_manifest.pkg_ident()).unwrap(); } + let note = note.trim().into(); return Err(crate::Error { message: t!("info.error_pkgspec_not_unique").into(), @@ -62,13 +63,23 @@ fn print_pkgmanifest(pkg_manifest: &PkgManifest) { pkg_manifest.arch ); } - println!("{}:", t!("info.print_pkgmanifest.dependencies")); - for i in pkg_manifest.dependencies.iter() { - println!(" - {i}"); + if !pkg_manifest.maintainers.is_empty() { + println!("{}:", t!("info.print_pkgmanifest.maintainers")); + for i in pkg_manifest.maintainers.iter() { + println!(" - {i}"); + } } - println!("{}:", t!("info.print_pkgmanifest.recommendations")); - for i in pkg_manifest.recommendations.iter() { - println!(" - {i}"); + if !pkg_manifest.dependencies.is_empty() { + println!("{}:", t!("info.print_pkgmanifest.dependencies")); + for i in pkg_manifest.dependencies.iter() { + println!(" - {i}"); + } + } + if !pkg_manifest.recommendations.is_empty() { + println!("{}:", t!("info.print_pkgmanifest.recommendations")); + for i in pkg_manifest.recommendations.iter() { + println!(" - {i}"); + } } } diff --git a/cli/src/install.rs b/cli/src/install.rs index b2d80f3..502d8bc 100644 --- a/cli/src/install.rs +++ b/cli/src/install.rs @@ -65,7 +65,7 @@ pub fn main(cli: Cli) -> Result<(), crate::Error> { ); if cli.yes { eprintln!("y"); - } else if !yesno(&t!("install.confirm_installation")) { + } else if !yesno(&t!("install.confirm_installation"), true) { eprintln!("{}", t!("install.aborting")); return Ok(()); } diff --git a/cli/src/remove.rs b/cli/src/remove.rs index 5c2af02..ff017c2 100644 --- a/cli/src/remove.rs +++ b/cli/src/remove.rs @@ -4,8 +4,10 @@ use crate::{ }; use anyhow::anyhow; use clap::Parser; +use console::style; use packie::{PackieBuilder, package::PkgSpec}; use rust_i18n::t; +use std::fmt::Write; #[derive(Debug, Parser)] pub struct Cli { @@ -21,18 +23,33 @@ pub fn main(cli: Cli) -> Result<(), crate::Error> { for pkgspec in cli.items { let mut found = packie.search_installation(&pkgspec)?; if found.is_empty() { - return Err(error!("remove.package_not_found")); + return Err(error!("remove.package_not_found", pkgspec = pkgspec)); } if found.len() != 1 { - return Err(error!("remove.pkgspec_not_unique")); + let mut note = String::new(); + writeln!(&mut note, "{}", t!("remove.note_pkgspec_not_unique")).unwrap(); + for i in found.iter() { + writeln!(&mut note, " - {}", i.pkg_manifest.pkg_ident()).unwrap(); + } + let note = note.trim().into(); + + return Err(crate::Error { + message: t!("remove.error_pkgspec_not_unique", pkgspec = pkgspec).into(), + note: Some(note), + }); } packages.push(found.remove(0)); } println!("{}", t!("remove.prompt_to_remove")); - print_list(4, packages.iter().map(|x| x.install_pkgspec.to_string())); + print_list( + 4, + packages + .iter() + .map(|x| style(&x.install_pkgspec).green().to_string()), + ); if cli.yes { println!("y"); - } else if !yesno(&t!("remove.confirm_removal")) { + } else if !yesno(&t!("remove.confirm_removal"), false) { println!("{}", t!("remove.aborting")); return Ok(()); } diff --git a/locales/libpackie/main.yml b/locales/libpackie/main.yml index f54c5a9..d3f4696 100644 --- a/locales/libpackie/main.yml +++ b/locales/libpackie/main.yml @@ -59,3 +59,13 @@ BuildPackieError.Lock: en: "failed to lock packie database: %{error}" zh-CN: "无法锁定 Packie 数据库:%{error}" ja: "Packie データベースのロックに失敗しました:%{error}" +version.VersionError.ParseNamespace: + en: "invalid version namespace" + zh-CN: "无效的版本命名空间" + ja: "" +version.VersionError.ParseMain: + en: "invalid version code" + zh-CN: "无效的版本号" +version.VersionError.ParsePrerelease: + en: "invalid version prerelease" + zh-CN: "无效的预发布版本" diff --git a/locales/packie-cli/main.yml b/locales/packie-cli/main.yml index d515940..a121730 100644 --- a/locales/packie-cli/main.yml +++ b/locales/packie-cli/main.yml @@ -71,6 +71,10 @@ info.print_pkgmanifest.description: en: "Description" zh-CN: "描述" ja: "説明" +info.print_pkgmanifest.maintainers: + en: "Maintainers" + zh-CN: "维护者" + ja: "メンテナー" info.print_install_misc.install_pkgspec: en: "Install package spec" zh-CN: "安装请求" @@ -84,13 +88,17 @@ info.print_install_misc.install_date: zh-CN: "安装日期" ja: "インストール日時" remove.package_not_found: - en: "The specified package was not found." - zh-CN: "找不到指定的包。" - ja: "指定されたパッケージが見つかりません。" -remove.pkgspec_not_unique: - en: "The specified package spec was not unique." - zh-CN: "指定的包规范匹配了多个包。" - ja: "指定されたパッケージ指定が複数のパッケージに一致しました。" + en: "The specified package \"%{pkgspec}\" was not found." + zh-CN: "找不到指定的包 \"%{pkgspec}\"。" + ja: "指定されたパッケージ \"%{pkgspec}\" が見つかりません。" +remove.error_pkgspec_not_unique: + en: "The specified package spec \"%{pkgspec}\" was not unique." + zh-CN: "指定的包规范 \"%{pkgspec}\" 匹配了多个包。" + ja: "指定されたパッケージ指定 \"%{pkgspec}\" が複数のパッケージに一致しました。" +remove.note_pkgspec_not_unique: + en: "The following packages were matched:" + zh-CN: "匹配到了下面的包:" + ja: "以下のパッケージが一致しました:" remove.action_failure: en: "Failed to remove the specified package: %{error}" zh-CN: "无法移除指定的包:%{error}" diff --git a/src/common/fs.rs b/src/common/fs.rs index bbb3e15..e2c18af 100644 --- a/src/common/fs.rs +++ b/src/common/fs.rs @@ -1,4 +1,5 @@ use rust_i18n::t; +use rustc_hash::FxHashSet; use std::{ fs::{FileType, ReadDir}, path::{Path, PathBuf}, @@ -65,12 +66,20 @@ pub struct TreeDirEntry { } pub fn du_dir(path: impl AsRef) -> std::io::Result { + use std::os::unix::fs::MetadataExt; + let mut size = 0; + let mut inodes = FxHashSet::default(); let path = path.as_ref(); for ent in TreeDir::new(path)? { let ent = ent?; let metadata = std::fs::symlink_metadata(path.join(&ent.relpath))?; + let inode = (metadata.dev(), metadata.ino()); + if inodes.contains(&inode) { + continue; + } size += metadata.len(); + inodes.insert(inode); } Ok(size) } diff --git a/src/package.rs b/src/package.rs index 1151df4..5f393ef 100644 --- a/src/package.rs +++ b/src/package.rs @@ -88,6 +88,10 @@ pub struct PkgManifest { #[serde(default)] pub description: String, + /// Package maintainers. + #[serde(default)] + pub maintainers: Vec, + /// Package dependencies. #[serde(default)] pub dependencies: Vec, @@ -122,6 +126,9 @@ impl PkgManifest { } } +/// Characters that cannot appear in a package name. +const PKGNAME_FORBIDDEN_CHARS: &str = " ();@'\"*!#$%^&[]{}\\/:<>?~`=\n\t\r"; + /// A package specifier. #[derive(Debug, Clone)] pub struct PkgSpec { @@ -149,7 +156,7 @@ impl From for PkgSpec { } } impl FromStr for PkgSpec { - type Err = crate::version::VersionError; + type Err = ParseError; fn from_str(s: &str) -> Result { if let Ok(pkgident) = PkgIdent::from_str(s) { @@ -202,6 +209,9 @@ impl FromStr for PkgSpec { arch = Some(arch_str); } let name = name.trim_end().to_string(); + if name.contains(|ch| PKGNAME_FORBIDDEN_CHARS.contains(ch)) { + return Err(ParseError::Pkgname(name)); + } Ok(Self { name, @@ -237,7 +247,7 @@ impl Display for PkgIdent { } } impl FromStr for PkgIdent { - type Err = crate::version::VersionError; + type Err = ParseError; fn from_str(s: &str) -> Result { let mut name = String::new(); @@ -257,8 +267,12 @@ impl FromStr for PkgIdent { arch.push(c); } } + let name: String = name.trim().into(); + if name.contains(|ch| PKGNAME_FORBIDDEN_CHARS.contains(ch)) { + return Err(ParseError::Pkgname(name)); + } Ok(Self { - name: name.trim().into(), + name, version: version.trim().parse()?, arch: arch.trim().into(), }) @@ -294,3 +308,18 @@ impl FromStr for AbsPkgIdent { } } crate::impl_serde_str!(AbsPkgIdent); + +/// An error caused by parsing package spec or ident. +#[derive(Debug, thiserror::Error)] +pub enum ParseError { + #[error("invalid package name: {0}")] + Pkgname(String), + + #[error("{0}")] + Version(crate::version::VersionError), +} +impl From for ParseError { + fn from(value: crate::version::VersionError) -> Self { + Self::Version(value) + } +} diff --git a/src/version.rs b/src/version.rs index 538b344..d511dbd 100644 --- a/src/version.rs +++ b/src/version.rs @@ -1,6 +1,7 @@ //! Parsing and matching of software versions. use std::{cmp::Ordering, fmt::Display, str::FromStr}; +use rust_i18n::t; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Version { @@ -131,6 +132,9 @@ pub enum VersionFilter { #[default] Any, + /// The '..., ..., ...' filter. + All(Vec), + /// The '<=' filter. Lte(Version), @@ -140,11 +144,11 @@ pub enum VersionFilter { /// The '=' filter. Eq(Version), - /// The '>=xxx, <=xxx' filter. - Between(Version, Version), - /// The '~' (or default) filter. Compatible(Version), + + /// The '+' filter. + WithMeta(String), } impl VersionFilter { pub fn matches(&self, version: &Version) -> bool { @@ -153,8 +157,9 @@ impl VersionFilter { Self::Lte(other) => version <= other, Self::Gte(other) => version >= other, Self::Eq(other) => version == other, - Self::Between(a, b) => version >= a && version <= b, + Self::All(cond) => cond.iter().all(|x| x.matches(version)), Self::Compatible(other) => semver_is_compatible(version, other), + Self::WithMeta(meta) => version.meta.as_deref() == Some(meta), } } } @@ -165,6 +170,13 @@ impl FromStr for VersionFilter { if s == "*" { return Ok(Self::Any); } + if s.contains(',') { + let mut all = Vec::new(); + for i in s.split(',') { + all.push(i.trim().parse()?); + } + return Ok(Self::All(all)); + } if let Some(version) = s.strip_prefix('=') { return Ok(Self::Eq(version.parse()?)); } @@ -174,19 +186,8 @@ impl FromStr for VersionFilter { if let Some(version) = s.strip_prefix("<=") { return Ok(Self::Lte(version.parse()?)); } - if let Some((a, b)) = s.split_once(',') { - let (mut smaller, mut greater) = (None, None); - for i in [a.trim(), b.trim()] { - if let Some(version) = i.strip_prefix(">=") { - smaller = Some(version.parse()?); - } else if let Some(version) = i.strip_prefix("<=") { - greater = Some(version.parse()?); - } - } - return Ok(Self::Between( - smaller.ok_or(VersionError::InvalidRange)?, - greater.ok_or(VersionError::InvalidRange)?, - )); + if let Some(meta) = s.strip_prefix("+") { + return Ok(Self::WithMeta(meta.into())); } if let Some(version) = s.strip_prefix("~") { return Ok(Self::Compatible(version.parse()?)); @@ -201,8 +202,16 @@ impl Display for VersionFilter { Self::Lte(ver) => write!(f, "<={ver}"), Self::Gte(ver) => write!(f, ">={ver}"), Self::Eq(ver) => write!(f, "={ver}"), - Self::Between(a, b) => write!(f, ">={a}, <={b}"), + Self::All(cond) => write!( + f, + "{}", + cond.iter() + .map(ToString::to_string) + .collect::>() + .join(", ") + ), Self::Compatible(ver) => write!(f, "~{ver}"), + Self::WithMeta(meta) => write!(f, "+{meta}"), } } } @@ -211,17 +220,14 @@ crate::impl_serde_str!(VersionFilter); /// An error caused by dealing with version or version filter strings. #[derive(Debug, Clone, thiserror::Error)] pub enum VersionError { - #[error("invalid namespace")] + #[error("{}", t!("version.VersionError.ParseNamespace"))] ParseNamespace, - #[error("invalid main part")] + #[error("{}", t!("version.VersionError.ParseMain"))] ParseMain, - #[error("invalid prerelease part")] + #[error("{}", t!("version.VersionError.ParsePrerelease"))] ParsePrerelease, - - #[error("invalid version range")] - InvalidRange, } fn semver_is_compatible(a: &Version, b: &Version) -> bool {