initial commit

Signed-off-by: sisungo <[email protected]>
This commit is contained in:
2026-08-21 10:01:06 +00:00
commit 55b40267ff
36 changed files with 3588 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "packie-cli"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "packie"
path = "src/main.rs"
[dependencies]
anyhow = "1"
chrono = "0.4"
clap = { version = "4", features = ["derive"] }
console = "0.16"
nix = { version = "0.31", features = ["sched"] }
itertools = "0.15"
packie = { path = "../" }
rust-i18n = "4"
indicatif = "0.18"
dialoguer = "0.12"
+78
View File
@@ -0,0 +1,78 @@
use clap::Parser;
use packie::PackieBuilder;
use rust_i18n::t;
use std::path::PathBuf;
#[derive(Debug, Parser)]
pub struct Cli {
#[arg(short, long, default_value = "true")]
packages: bool,
#[arg(short, long)]
verbose: bool,
}
pub fn main(cli: Cli) -> Result<(), crate::Error> {
let packie = PackieBuilder::new().build()?;
let repos_dir = packie.cache().repos_dir();
if cli.packages {
clean_packages(&cli, &repos_dir);
}
Ok(())
}
fn clean_packages(cli: &Cli, repos_dir: &PathBuf) {
let read_repos_dir_errx =
|e: &std::io::Error| eprintln!("{}", t!("clean.warn_repos_dir", error = e));
let Ok(repos) = std::fs::read_dir(&repos_dir).inspect_err(read_repos_dir_errx) else {
return;
};
for repo in repos {
let Ok(repo) = repo.inspect_err(read_repos_dir_errx) else {
break;
};
let read_repo_dir_errx = |e: &std::io::Error| {
eprintln!(
"{}",
t!(
"clean.warn_repo_dir",
name = repo.file_name().display().to_string(),
error = e
)
)
};
let Ok(repo) = std::fs::read_dir(repo.path()).inspect_err(read_repo_dir_errx) else {
continue;
};
for file in repo {
let Ok(file) = file.inspect_err(read_repo_dir_errx) else {
break;
};
if file.file_name().as_encoded_bytes().ends_with(b".pkg") {
match std::fs::remove_file(file.path()) {
Ok(()) => {
if cli.verbose {
eprintln!(
"{}",
t!("clean.info_remove_file", path = file.path().display())
);
}
}
Err(err) => {
eprintln!(
"{}",
t!(
"clean.warn_remove_file",
path = file.path().display(),
error = err
)
);
}
};
}
}
}
}
+80
View File
@@ -0,0 +1,80 @@
use chrono::{Local, TimeZone};
use console::{Term, style};
use indicatif::ProgressStyle;
use rust_i18n::t;
pub fn progress_style_multidownload() -> ProgressStyle {
let mut lines = Vec::with_capacity(3);
let (_, cols) = Term::stderr().size();
let short_first_line = format!("{{spinner}} {prefix}", prefix = style("{prefix}").green());
let info_line = format!(
"{speed}{{bytes_per_sec}} {current}{{bytes}} {total}{{total_bytes}} {eta}{{eta}}",
speed = t!("common.progress_style.speed"),
current = t!("common.progress_style.current"),
total = t!("common.progress_style.total"),
eta = t!("common.progress_style.eta"),
);
let long_first_line = format!("{short_first_line} {info_line}");
if cols > 100 {
lines.push(&long_first_line[..]);
} else {
lines.push(&short_first_line[..]);
lines.push(&info_line[..]);
}
lines.push("{wide_bar}");
ProgressStyle::with_template(&lines.join("\n")).unwrap()
}
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<Item = impl AsRef<str>>) {
let (_, cols) = Term::stdout().size();
let mut line_used: usize = 0;
for i in list {
let mut i = i.as_ref().to_string();
if line_used >= cols 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.),
}
}
pub fn format_datetime(timestamp: i64) -> String {
let Some(datetime) = Local.timestamp_opt(timestamp, 0).earliest() else {
return "?".into();
};
datetime.to_string()
}
+102
View File
@@ -0,0 +1,102 @@
use crate::{common::format_size, error};
use clap::Parser;
use packie::{
PackieBuilder,
local::Installation,
package::{PkgManifest, PkgSpec},
};
use rust_i18n::t;
use std::fmt::Write;
#[derive(Debug, Parser)]
pub struct Cli {
pkgspec: PkgSpec,
}
pub fn main(cli: Cli) -> Result<(), crate::Error> {
let mut packie = PackieBuilder::new().readonly(true).build()?;
let mut found = packie.search_installation(&cli.pkgspec)?;
if found.len() == 0 {
return Err(error!("info.no_package_found"));
}
if found.len() != 1 {
let mut note = String::new();
writeln!(&mut note, "{}", t!("info.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!("info.error_pkgspec_not_unique").into(),
note: Some(note),
});
}
let found = found.remove(0);
print_pkgmanifest(&found.pkg_manifest);
print_install_misc(&found);
Ok(())
}
fn print_pkgmanifest(pkg_manifest: &PkgManifest) {
println!(
"{}: {}",
t!("info.print_pkgmanifest.name"),
pkg_manifest.name
);
println!(
"{}: {}",
t!("info.print_pkgmanifest.version"),
pkg_manifest.version
);
println!(
"{}: {}",
t!("info.print_pkgmanifest.arch"),
pkg_manifest.arch
);
if !pkg_manifest.description.is_empty() {
println!(
"{}: {}",
t!("info.print_pkgmanifest.description"),
pkg_manifest.arch
);
}
if !pkg_manifest.maintainers.is_empty() {
println!("{}:", t!("info.print_pkgmanifest.maintainers"));
for i in pkg_manifest.maintainers.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}");
}
}
}
fn print_install_misc(installation: &Installation) {
println!(
"{}: {}",
t!("info.print_install_misc.install_pkgspec"),
installation.install_pkgspec
);
println!(
"{}: {}",
t!("info.print_install_misc.install_date"),
crate::common::format_datetime(installation.install_date),
);
println!(
"{}: {}",
t!("info.print_install_misc.installed_size"),
format_size(installation.installed_size),
);
}
+10
View File
@@ -0,0 +1,10 @@
use clap::Parser;
use packie::PackieBuilder;
#[derive(Debug, Parser)]
pub struct Cli {}
pub fn main(_: Cli) -> Result<(), crate::Error> {
PackieBuilder::new().build()?;
Ok(())
}
+106
View File
@@ -0,0 +1,106 @@
use crate::common::{format_size, print_list, yesno};
use clap::Parser;
use console::style;
use indicatif::ProgressBar;
use itertools::Itertools;
use packie::{
PackieBuilder, install::InstallOptions, package::PkgSpec, repo::DownloadPackageEvent,
};
use rust_i18n::t;
#[derive(Debug, Parser)]
pub struct Cli {
/// Force the specified items to be files
#[arg(short, long)]
file: bool,
/// Don't require manual confirmation
#[arg(short, long)]
yes: bool,
/// Items to be installed
items: Vec<String>,
}
pub fn main(cli: Cli) -> Result<(), crate::Error> {
let mut packie = PackieBuilder::new().build()?;
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>()?)?);
}
let packages: Vec<_> = packages
.into_iter()
.unique_by(|x| x.repo_pkg.pkg_manifest.pkg_ident())
.collect();
if packages.is_empty() {
eprintln!("{}", t!("install.no_package_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| style(&x.pkgspec).green().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"), true) {
eprintln!("{}", t!("install.aborting"));
return Ok(());
}
let progress_bar =
ProgressBar::new(download_size).with_style(crate::common::progress_style_multidownload());
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));
progress_bar.set_prefix(package.repo_pkg.pkg_manifest.pkg_ident().to_string());
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);
progress_bar.set_position(init_pos + package.repo_pkg.download_size);
}
drop(progress_bar);
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(())
}
+130
View File
@@ -0,0 +1,130 @@
mod clean;
mod common;
mod info;
mod initdb;
mod install;
mod print;
mod remove;
mod repo_admin;
mod select;
mod update;
use clap::Parser;
use console::style;
use rust_i18n::t;
use std::path::{Path, PathBuf};
rust_i18n::i18n!("../locales/packie-cli");
#[derive(Debug, Parser)]
struct Cli {
#[arg(long)]
chroot: Option<PathBuf>,
#[command(subcommand)]
subcommand: Subcommand,
}
#[derive(Debug, Parser)]
enum Subcommand {
/// Clean cache data
Clean(clean::Cli),
/// Get package information
Info(info::Cli),
/// Explicitly initialize the database
InitDb(initdb::Cli),
/// Install one or more packages
Install(install::Cli),
/// Print configuration information
Print(print::Cli),
/// Remove one or more packages
Remove(remove::Cli),
/// Administrate repository service
RepoAdmin(repo_admin::Cli),
/// Update repository indexes
Update(update::Cli),
/// Select global link
Select(select::Cli),
}
fn main() {
rust_i18n::set_locale(match std::env::var("LANG").as_deref() {
Ok("zh_CN.UTF-8") => "zh-CN",
Ok("ja_JP.UTF-8") => "ja",
_ => "en",
});
let cli = Cli::parse();
if let Some(new_root) = cli.chroot
&& let Err(err) = chroot(&new_root)
{
eprintln!("{}", style(t!("main.error", error = err)).red());
std::process::exit(1);
}
let result = match cli.subcommand {
Subcommand::Clean(cli) => clean::main(cli),
Subcommand::Info(cli) => info::main(cli),
Subcommand::InitDb(cli) => initdb::main(cli),
Subcommand::Install(cli) => install::main(cli),
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),
Subcommand::Select(cli) => select::main(cli),
};
if let Err(err) = result {
eprintln!("{}", style(t!("main.error", error = err.message)).red());
if let Some(note) = err.note {
eprintln!("{}", style(t!("main.note", note = note)).blue());
}
std::process::exit(1);
}
}
#[cfg(target_os = "linux")]
fn chroot(new_root: &Path) -> std::io::Result<()> {
nix::sched::unshare(nix::sched::CloneFlags::CLONE_NEWUSER)?;
nix::sched::unshare(nix::sched::CloneFlags::CLONE_NEWNS)?;
std::os::unix::fs::chroot(new_root)?;
std::env::set_current_dir("/")?;
Ok(())
}
#[cfg(all(target_family = "unix", not(target_os = "linux")))]
fn chroot(new_root: &Path) -> std::io::Result<()> {
std::os::unix::fs::chroot(new_root)?;
std::env::set_current_dir("/")?;
Ok(())
}
#[derive(Debug)]
struct Error {
message: String,
note: Option<String>,
}
impl<E: ToString> From<E> for Error {
fn from(value: E) -> Self {
Self {
message: value.to_string(),
note: None,
}
}
}
#[macro_export]
macro_rules! error {
($($all:tt)*) => {
$crate::Error {
message: ::rust_i18n::t!($($all)*).into(),
note: ::std::option::Option::None,
}
};
}
+46
View File
@@ -0,0 +1,46 @@
use anyhow::anyhow;
use clap::Parser;
use packie::PackieBuilder;
#[derive(Debug, Parser)]
pub struct Cli {
key: String,
}
pub fn main(cli: Cli) -> Result<(), crate::Error> {
let key: Vec<&str> = cli.key.split('.').collect();
let result = match key.get(0).copied().unwrap() {
"profile" => profile(&key[1..]),
"package" => package(&key[1..]),
unknown => Err(anyhow!("unknown key {unknown} to print")),
};
println!("{}", result?);
Ok(())
}
fn profile(other: &[&str]) -> anyhow::Result<String> {
let packie = PackieBuilder::new().readonly(true).build()?;
match other {
["host_arch"] => Ok(packie.profile().host_arch.clone()),
_ => Err(anyhow!("unsupported profile key")),
}
}
fn package(other: &[&str]) -> anyhow::Result<String> {
let mut packie = PackieBuilder::new().readonly(true).build()?;
match other {
["prefix", ..] => {
let mut found = packie.search_installation(&other[1..].join(".").parse()?)?;
if found.is_empty() {
return Err(anyhow!("package not found"));
}
Ok(packie
.profile()
.pkg_dir
.join(found.remove(0).pkg_manifest.pkg_ident().prefix_name())
.to_string_lossy()
.into())
}
_ => Err(anyhow!("unsupported profile key")),
}
}
+55
View File
@@ -0,0 +1,55 @@
use crate::{
common::{print_list, yesno},
error,
};
use anyhow::anyhow;
use clap::Parser;
use console::style;
use packie::{PackieBuilder, package::PkgSpec};
use rust_i18n::t;
#[derive(Debug, Parser)]
pub struct Cli {
#[arg(short, long)]
yes: bool,
items: Vec<PkgSpec>,
}
pub fn main(cli: Cli) -> Result<(), crate::Error> {
let mut packie = PackieBuilder::new().build()?;
let mut packages = Vec::with_capacity(cli.items.len());
for pkgspec in cli.items {
let mut found = packie.search_installation(&pkgspec)?;
if found.is_empty() {
return Err(error!("remove.package_not_found"));
}
if found.len() != 1 {
return Err(error!("remove.pkgspec_not_unique"));
}
packages.push(found.remove(0));
}
println!("{}", t!("remove.prompt_to_remove"));
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"), false) {
println!("{}", t!("remove.aborting"));
return Ok(());
}
for i in packages {
println!(
"{}",
t!("remove.action_hint", package = i.pkg_manifest.pkg_ident())
);
packie
.remove(&i.pkg_manifest.pkg_ident().into())
.map_err(|error| anyhow!("{}", t!("remove.action_failure", error = error)))?;
}
Ok(())
}
+57
View File
@@ -0,0 +1,57 @@
use anyhow::anyhow;
use clap::Parser;
use indicatif::ProgressBar;
use packie::{
package::{Package, PkgIdent},
repo::RepoServeDir,
};
use std::path::PathBuf;
#[derive(Debug, Parser)]
pub struct Cli {
/// Path of the repository serve directory
repo: PathBuf,
/// Add package(s) to the repository
#[arg(long, short)]
add_package: Option<Vec<PathBuf>>,
/// Remove package(s) from the repository
#[arg(long, short)]
remove_package: Option<Vec<PkgIdent>>,
/// Clone or pull remote repository
#[arg(long, short)]
clone: Option<String>,
}
pub fn main(cli: Cli) -> Result<(), crate::Error> {
if let Some(clone) = cli.clone {
let progress_bar = ProgressBar::new(0);
RepoServeDir::clone(&clone.parse()?, &cli.repo, |pkg, url, sum, total| {
progress_bar.set_length(total);
progress_bar.set_position(sum);
if let Some(pkg) = pkg {
progress_bar.set_prefix(pkg);
}
progress_bar.set_message(url.to_string());
})
.map_err(|x| anyhow!("{x}"))?;
}
let mut repo = RepoServeDir::open(cli.repo)?;
for pkg in cli.remove_package.into_iter().flatten() {
repo.remove_package(&pkg)
.map_err(|err| anyhow!("\"{pkg}\": {err}"))?;
}
for file in cli.add_package.into_iter().flatten() {
let pkg_ident = Package::open(&file)?.manifest()?.pkg_ident();
_ = repo.remove_package(&pkg_ident);
repo.add_package(&file)
.map_err(|err| anyhow!("\"{}\": {}", file.display(), err))?;
}
Ok(())
}
+69
View File
@@ -0,0 +1,69 @@
use crate::error;
use clap::Parser;
use console::style;
use dialoguer::Select;
use packie::PackieBuilder;
use rust_i18n::t;
use std::path::PathBuf;
#[derive(Debug, Parser)]
pub struct Cli {
file: PathBuf,
}
pub fn main(cli: Cli) -> Result<(), crate::Error> {
let mut packie = PackieBuilder::new().build()?;
let file = if cli.file.is_absolute() {
cli.file.clone()
} else {
std::env::current_dir()?.join(&cli.file)
};
let mut all_pkgs = Vec::with_capacity(1024);
let mut candidates = Vec::new();
packie.for_each_installation::<anyhow::Error>(|inst| {
all_pkgs.push(inst.pkg_manifest.pkg_ident());
Ok(())
})?;
for pkgident in all_pkgs {
let links = packie.links_of_pkg(&pkgident);
let Ok(links) = links.iter() else {
return Ok(());
};
if let Some(link) = links
.filter_map(|x| x.ok())
.find(|x| x.to == file.to_string_lossy())
{
candidates.push((pkgident, link));
}
}
if candidates.is_empty() {
return Err(crate::Error {
message: t!("select.empty_list").into(),
note: Some(t!("select.note_relative_path").into()),
});
}
let items = std::iter::once(t!("select.disabled").into())
.chain(candidates.iter().map(|x| x.0.to_string()))
.map(|x| style(x).green());
let choice = Select::new()
.with_prompt(t!("select.prompt_select_package"))
.items(items)
.interact()?;
if choice == 0 {
if let Err(err) = packie::link::remove(&file) {
if err.kind() == std::io::ErrorKind::InvalidData {
return Err(error!("select.not_a_link"));
}
if err.kind() == std::io::ErrorKind::NotFound {
return Ok(());
}
Err(err)?;
}
return Ok(());
}
let (pkgident, linkdes) = candidates.remove(choice - 1);
_ = packie::link::remove(&file);
_ = packie::link::deactivate_by_description(&mut packie, &pkgident, &linkdes);
packie::link::activate_by_description(&mut packie, &pkgident, &linkdes)?;
Ok(())
}
+40
View File
@@ -0,0 +1,40 @@
use clap::Parser;
use indicatif::ProgressBar;
use packie::{PackieBuilder, repo::SyncEvent};
use rust_i18n::t;
#[derive(Debug, Parser)]
pub struct Cli {}
pub fn main(cli: Cli) -> Result<(), crate::Error> {
let mut packie = PackieBuilder::new().build()?;
let progress_bar =
ProgressBar::new(0).with_style(crate::common::progress_style_multidownload());
packie
.sync_repo()
.on_event(|ev| match ev {
SyncEvent::BeginRepo(repo) => {
progress_bar.set_prefix(repo.clone());
progress_bar.println(t!("update.begin_repo", repo = repo));
}
SyncEvent::Progress(progress) => {
progress_bar.set_length(progress.total_bytes);
progress_bar.set_position(progress.downloaded_bytes);
}
SyncEvent::Error(repo, url, err) => match (repo, url) {
(Some(repo), Some(url)) => {
progress_bar.println(format!(
"failed to update repository \"{repo}\" from \"{url}\": {err}"
));
}
(Some(repo), None) => {
progress_bar.println(format!("failed to update repository \"{repo}\": {err}"));
}
_ => {
progress_bar.println(format!("failed to update repositories: {err}"));
}
},
})
.run();
Ok(())
}