@@ -0,0 +1,7 @@
|
|||||||
|
/target
|
||||||
|
/Cargo.lock
|
||||||
|
/.zed
|
||||||
|
/.idea
|
||||||
|
/.vscode
|
||||||
|
.DS_Store
|
||||||
|
._.DS_Store
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
[package]
|
||||||
|
name = "packie"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["zstd", "gzip"]
|
||||||
|
zstd = ["dep:zstd"]
|
||||||
|
gzip = ["dep:flate2"]
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["cli"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
const-hex = "1"
|
||||||
|
digest = "0.11"
|
||||||
|
flate2 = { version = "1", optional = true }
|
||||||
|
rustc-hash = "2"
|
||||||
|
rust-i18n = "4"
|
||||||
|
rusqlite = { version = "0.40", features = ["bundled"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
sha3 = "0.12"
|
||||||
|
tar = "0.4"
|
||||||
|
tempfile = "3"
|
||||||
|
thiserror = "2"
|
||||||
|
toml = "1"
|
||||||
|
itertools = "0.15"
|
||||||
|
ureq = { version = "3", features = ["socks-proxy"] }
|
||||||
|
url = { version = "2", features = ["serde"] }
|
||||||
|
nix = { version = "0.31", features = ["fs"] }
|
||||||
|
zstd = { version = "0.13", optional = true }
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
# Building & Installing Packie
|
||||||
|
|
||||||
|
## Building Packie
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
These environment variables affect building of Packie:
|
||||||
|
|
||||||
|
- `PACKIE_HOST_ARCH`: Host architecture.
|
||||||
|
- `PACKIE_PKG_DIR`: Package bundle directory.
|
||||||
|
- `PACKIE_DATA_DIR`: Packie data directory.
|
||||||
|
- `PACKIE_CONFIG_DIR`: Packie config directory.
|
||||||
|
- `PACKIE_CACHE_DIR`: Packie cache directory.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
Copyright © 2026 sisungo
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
and associated documentation files (the “Software”), to deal in the Software without
|
||||||
|
restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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(())
|
||||||
|
}
|
||||||
@@ -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
@@ -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,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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")),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(())
|
||||||
|
}
|
||||||
@@ -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(())
|
||||||
|
}
|
||||||
@@ -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(())
|
||||||
|
}
|
||||||
@@ -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(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
_version: 2
|
||||||
|
common.CopyError:
|
||||||
|
en: 'failed to copy from "%{src}" to "%{dst}": %{error}'
|
||||||
|
zh-CN: '无法将 "%{src}" 复制到 "%{dst}":%{error}'
|
||||||
|
ja: '"%{src}" から "%{dst}" へのコピーに失敗しました:%{error}'
|
||||||
|
install.InstallError.Database:
|
||||||
|
en: "local database error: %{error}"
|
||||||
|
zh-CN: "本地数据库错误:%{error}"
|
||||||
|
ja: "ローカルデータベースエラー:%{error}"
|
||||||
|
install.InstallError.NotFound:
|
||||||
|
en: "item not found: %{item}"
|
||||||
|
zh-CN: "找不到项目:%{item}"
|
||||||
|
ja: "項目が見つかりません:%{item}"
|
||||||
|
install.InstallError.DependencyRing:
|
||||||
|
en: "dependency ring detected"
|
||||||
|
zh-CN: "检测到依赖环"
|
||||||
|
ja: "依存関係の循環が検出されました"
|
||||||
|
remove.RemoveError.NotUniquePkgSpec:
|
||||||
|
en: "not unique package spec"
|
||||||
|
zh-CN: "指定的包范围匹配到了多个包"
|
||||||
|
ja: "パッケージ指定が一意ではありません"
|
||||||
|
remove.RemoveError.Database:
|
||||||
|
en: "local database error: %{error}"
|
||||||
|
zh-CN: "本地数据库错误:%{error}"
|
||||||
|
ja: "ローカルデータベースエラー:%{error}"
|
||||||
|
remove.RemoveError.NotFound:
|
||||||
|
en: "package not found"
|
||||||
|
zh-CN: "找不到包"
|
||||||
|
ja: "パッケージが見つかりません"
|
||||||
|
remove.RemoveError.RemoveFiles:
|
||||||
|
en: "failed to remove package files: %{error}"
|
||||||
|
zh-CN: "无法删除包文件:%{error}"
|
||||||
|
ja: "パッケージファイルの削除に失敗しました:%{error}"
|
||||||
|
install.InstallError.AlreadyInstalled:
|
||||||
|
en: "package already installed"
|
||||||
|
zh-CN: "此包已经安装"
|
||||||
|
ja: "パッケージはすでにインストールされています"
|
||||||
|
repo.RepoError.Database:
|
||||||
|
en: "repository database error: %{error}"
|
||||||
|
zh-CN: "软件包仓库数据库错误:%{error}"
|
||||||
|
ja: "リポジトリデータベースエラー:%{error}"
|
||||||
|
repo.RepoError.NoSuchRepo:
|
||||||
|
en: "no such repository"
|
||||||
|
zh-CN: "找不到仓库"
|
||||||
|
ja: "指定されたリポジトリが存在しません"
|
||||||
|
repo.RepoError.NoSuchPackage:
|
||||||
|
en: "no such package"
|
||||||
|
zh-CN: "找不到包"
|
||||||
|
ja: "指定されたパッケージが存在しません"
|
||||||
|
repo.RepoError.AllTriesFailed:
|
||||||
|
en: "all tries failed"
|
||||||
|
zh-CN: "所有尝试均失败"
|
||||||
|
ja: "すべての試行に失敗しました"
|
||||||
|
repo.download.DownloadError.Checksum:
|
||||||
|
en: "checksum failed"
|
||||||
|
zh-CN: "文件完整性检查失败"
|
||||||
|
ja: "チェックサム検証に失敗しました"
|
||||||
|
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 number"
|
||||||
|
zh-CN: "无效的版本号"
|
||||||
|
ja: "無効なバージョン番号です"
|
||||||
|
version.VersionError.ParsePrerelease:
|
||||||
|
en: "invalid version prerelease"
|
||||||
|
zh-CN: "无效的预发布版本"
|
||||||
|
ja: "無効なプレリリースバージョンです"
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
_version: 2
|
||||||
|
main.error:
|
||||||
|
en: "error: %{error}"
|
||||||
|
zh-CN: "错误:%{error}"
|
||||||
|
ja: "エラー:%{error}"
|
||||||
|
main.note:
|
||||||
|
en: "note: %{note}"
|
||||||
|
zh-CN: "备注:%{note}"
|
||||||
|
ja: "備考:%{note}"
|
||||||
|
update.begin_repo:
|
||||||
|
en: "Synchronizing repository \"%{repo}\"..."
|
||||||
|
zh-CN: "正在同步软件包仓库 \"%{repo}\"..."
|
||||||
|
ja: "リポジトリ \"%{repo}\" を同期中..."
|
||||||
|
install.prompt_to_install:
|
||||||
|
en: "The following packages are to be installed:"
|
||||||
|
zh-CN: "将要安装如下的包:"
|
||||||
|
ja: "以下のパッケージをインストールします:"
|
||||||
|
install.download_size:
|
||||||
|
en: "Download size: %{size}"
|
||||||
|
zh-CN: "下载体积:%{size}"
|
||||||
|
ja: "ダウンロードサイズ:%{size}"
|
||||||
|
install.installed_size:
|
||||||
|
en: "Installed size: %{size}"
|
||||||
|
zh-CN: "安装体积:%{size}"
|
||||||
|
ja: "インストール後サイズ:%{size}"
|
||||||
|
install.confirm_installation:
|
||||||
|
en: "Confirm to install?"
|
||||||
|
zh-CN: "确认安装?"
|
||||||
|
ja: "インストールを続行しますか?"
|
||||||
|
install.aborting:
|
||||||
|
en: "Aborting."
|
||||||
|
zh-CN: "中止。"
|
||||||
|
ja: "中止します。"
|
||||||
|
install.no_package_to_install:
|
||||||
|
en: "No packages to install."
|
||||||
|
zh-CN: "没有要安装的包。"
|
||||||
|
ja: "インストールするパッケージがありません。"
|
||||||
|
info.no_package_found:
|
||||||
|
en: "The specified package was not found."
|
||||||
|
zh-CN: "找不到指定的包。"
|
||||||
|
ja: "指定されたパッケージが見つかりません。"
|
||||||
|
info.error_pkgspec_not_unique:
|
||||||
|
en: "The specified package spec was not unique."
|
||||||
|
zh-CN: "指定的包规范匹配了多个包。"
|
||||||
|
ja: "指定されたパッケージ指定が複数のパッケージに一致しました。"
|
||||||
|
info.note_pkgspec_not_unique:
|
||||||
|
en: "The following packages were matched:"
|
||||||
|
zh-CN: "匹配到了下面的包:"
|
||||||
|
ja: "以下のパッケージが一致しました:"
|
||||||
|
info.print_pkgmanifest.name:
|
||||||
|
en: "Package name"
|
||||||
|
zh-CN: "包名"
|
||||||
|
ja: "パッケージ名"
|
||||||
|
info.print_pkgmanifest.version:
|
||||||
|
en: "Package version"
|
||||||
|
zh-CN: "版本"
|
||||||
|
ja: "バージョン"
|
||||||
|
info.print_pkgmanifest.arch:
|
||||||
|
en: "Package architecture"
|
||||||
|
zh-CN: "架构"
|
||||||
|
ja: "アーキテクチャ"
|
||||||
|
info.print_pkgmanifest.dependencies:
|
||||||
|
en: "Dependencies"
|
||||||
|
zh-CN: "依赖"
|
||||||
|
ja: "依存関係"
|
||||||
|
info.print_pkgmanifest.recommendations:
|
||||||
|
en: "Recommendations"
|
||||||
|
zh-CN: "建议安装"
|
||||||
|
ja: "推奨パッケージ"
|
||||||
|
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: "安装请求"
|
||||||
|
ja: "インストール要求"
|
||||||
|
info.print_install_misc.installed_size:
|
||||||
|
en: "Installed size"
|
||||||
|
zh-CN: "安装后大小"
|
||||||
|
ja: "インストール後サイズ"
|
||||||
|
info.print_install_misc.install_date:
|
||||||
|
en: "Install date"
|
||||||
|
zh-CN: "安装日期"
|
||||||
|
ja: "インストール日時"
|
||||||
|
remove.package_not_found:
|
||||||
|
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}"
|
||||||
|
ja: "指定されたパッケージの削除に失敗しました:%{error}"
|
||||||
|
remove.prompt_to_remove:
|
||||||
|
en: "The following packages are to be removed:"
|
||||||
|
zh-CN: "将要移除下面的包:"
|
||||||
|
ja: "以下のパッケージを削除します:"
|
||||||
|
remove.confirm_removal:
|
||||||
|
en: "Confirm to remove?"
|
||||||
|
zh-CN: "确认移除?"
|
||||||
|
ja: "削除を続行しますか?"
|
||||||
|
remove.aborting:
|
||||||
|
en: "Aborting."
|
||||||
|
zh-CN: "中止。"
|
||||||
|
ja: "中止します。"
|
||||||
|
remove.action_hint:
|
||||||
|
en: "Removing package %{package} ..."
|
||||||
|
zh-CN: "正在移除包 %{package} ..."
|
||||||
|
ja: "パッケージ %{package} を削除中 ..."
|
||||||
|
common.progress_style.speed:
|
||||||
|
en: "Speed: "
|
||||||
|
zh-CN: "速度:"
|
||||||
|
ja: "速度:"
|
||||||
|
common.progress_style.current:
|
||||||
|
en: "Current: "
|
||||||
|
zh-CN: "已下载:"
|
||||||
|
ja: "現在:"
|
||||||
|
common.progress_style.total:
|
||||||
|
en: "Total: "
|
||||||
|
zh-CN: "总共:"
|
||||||
|
ja: "合計:"
|
||||||
|
common.progress_style.eta:
|
||||||
|
en: "ETA: "
|
||||||
|
zh-CN: "剩余时间:"
|
||||||
|
ja: "残り時間:"
|
||||||
|
clean.warn_repos_dir:
|
||||||
|
en: "warning: failed to read repository cache directory: %{error}"
|
||||||
|
zh-CN: "警告:无法读取软件包仓库缓存目录:%{error}"
|
||||||
|
ja: "警告:リポジトリキャッシュディレクトリの読み取りに失敗しました:%{error}"
|
||||||
|
clean.warn_repo_dir:
|
||||||
|
en: "warning: failed to read cache directory of repository \"%{name}\": %{error}"
|
||||||
|
zh-CN: "警告:无法读取软件包仓库 \"%{name}\" 的缓存目录:%{error}"
|
||||||
|
ja: "警告:リポジトリ \"%{name}\" のキャッシュディレクトリの読み取りに失敗しました:%{error}"
|
||||||
|
clean.info_remove_file:
|
||||||
|
en: "Removing file \"%{path}\"."
|
||||||
|
zh-CN: "正在删除文件 \"%{path}\"。"
|
||||||
|
ja: "ファイル \"%{path}\" を削除中。"
|
||||||
|
clean.warn_remove_file:
|
||||||
|
en: "warning: failed to remove file \"%{path}\": %{error}"
|
||||||
|
zh-CN: "警告:无法删除文件 \"%{path}\":%{error}"
|
||||||
|
ja: "警告:ファイル \"%{path}\" の削除に失敗しました:%{error}"
|
||||||
|
select.prompt_select_package:
|
||||||
|
en: "Select the package to use for the link"
|
||||||
|
zh-CN: "选择要为此链接使用的包"
|
||||||
|
ja: "リンクに使用するパッケージを選択してください"
|
||||||
|
select.empty_list:
|
||||||
|
en: "No package provided the given link"
|
||||||
|
zh-CN: "没有包提供要求的链接"
|
||||||
|
ja: "指定されたリンクを提供するパッケージはありません"
|
||||||
|
select.note_relative_path:
|
||||||
|
en: "A relative path is specified, and Packie resolves path based on your current directory. If this is not expected, provide an absolute path instead."
|
||||||
|
zh-CN: "指定了相对路径,Packie 会根据您当前目录解析路径。如果不符合预期,请改用绝对路径。"
|
||||||
|
ja: "相対パスが指定されています。Packieはカレントディレクトリに基づいてパスを解決します。意図と異なる場合は、絶対パスを指定してください。"
|
||||||
|
select.disabled:
|
||||||
|
en: "Disabled"
|
||||||
|
zh-CN: "禁用"
|
||||||
|
ja: "無効"
|
||||||
|
select.not_a_link:
|
||||||
|
en: "The existing file is not a link managed by Packie. Please remove it manually."
|
||||||
|
zh-CN: "已存在的文件不是一个被 Packie 管理的链接。请手动删除该文件。"
|
||||||
|
ja: "既存のファイルはPackieで管理されているリンクではありません。手動で削除してください。"
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
use digest::{Digest, DynDigest};
|
||||||
|
use std::{fs::File, io::Read, path::Path};
|
||||||
|
|
||||||
|
pub const DEFAULT_HASHER: &str = "sha3-512";
|
||||||
|
|
||||||
|
pub fn find_hasher(s: &str) -> std::io::Result<Box<dyn DynDigest>> {
|
||||||
|
match s {
|
||||||
|
"sha3-512" => Ok(Box::new(sha3::Sha3_512::new())),
|
||||||
|
_ => Err(std::io::ErrorKind::InvalidInput.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hash_reader<R: Read>(hasher_name: &str, reader: &mut R) -> std::io::Result<String> {
|
||||||
|
let mut buf = [0u8; 1024];
|
||||||
|
let mut hasher = find_hasher(hasher_name)?;
|
||||||
|
loop {
|
||||||
|
let n = reader.read(&mut buf)?;
|
||||||
|
hasher.update(&buf[..n]);
|
||||||
|
if n == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(format!(
|
||||||
|
"{hasher_name}:{}",
|
||||||
|
const_hex::encode(hasher.finalize()),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_reader<R: Read>(reader: &mut R, expected: &str) -> std::io::Result<bool> {
|
||||||
|
let hasher = expected.split(':').next().unwrap();
|
||||||
|
let hash = hash_reader(hasher, reader)?;
|
||||||
|
Ok(hash == expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hash_file<P: AsRef<Path>>(hasher: &str, path: P) -> std::io::Result<String> {
|
||||||
|
hash_reader(hasher, &mut File::open(path)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_file<P: AsRef<Path>>(path: P, expected: &str) -> std::io::Result<bool> {
|
||||||
|
verify_reader(&mut File::open(path)?, expected)
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
use rust_i18n::t;
|
||||||
|
use rustc_hash::FxHashSet;
|
||||||
|
use std::{
|
||||||
|
fs::{FileType, ReadDir},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
time::SystemTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TreeDir {
|
||||||
|
stack: Vec<(ReadDir, PathBuf)>,
|
||||||
|
}
|
||||||
|
impl TreeDir {
|
||||||
|
pub fn new(path: impl Into<PathBuf>) -> std::io::Result<Self> {
|
||||||
|
let root_path = path.into();
|
||||||
|
let read_dir = std::fs::read_dir(&root_path)?;
|
||||||
|
Ok(Self {
|
||||||
|
stack: vec![(read_dir, PathBuf::new())],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Iterator for TreeDir {
|
||||||
|
type Item = std::io::Result<TreeDirEntry>;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
while let Some((read_dir, current_relpath)) = self.stack.last_mut() {
|
||||||
|
match read_dir.next() {
|
||||||
|
Some(Ok(entry)) => {
|
||||||
|
let file_name = entry.file_name();
|
||||||
|
let relpath = current_relpath.join(file_name);
|
||||||
|
|
||||||
|
let ty = match entry.file_type() {
|
||||||
|
Ok(ty) => ty,
|
||||||
|
Err(e) => return Some(Err(e)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if ty.is_dir() {
|
||||||
|
match std::fs::read_dir(entry.path()) {
|
||||||
|
Ok(sub_read_dir) => {
|
||||||
|
self.stack.push((sub_read_dir, relpath.clone()));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Some(Err(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Some(Ok(TreeDirEntry { relpath, ty }));
|
||||||
|
}
|
||||||
|
Some(Err(e)) => {
|
||||||
|
return Some(Err(e));
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.stack.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TreeDirEntry {
|
||||||
|
pub relpath: PathBuf,
|
||||||
|
pub ty: FileType,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn du_dir(path: impl AsRef<Path>) -> std::io::Result<u64> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy_dir(src: impl Into<PathBuf>, dst: impl AsRef<Path>) -> Result<(), CopyError> {
|
||||||
|
let src = src.into();
|
||||||
|
std::fs::create_dir(dst.as_ref()).map_err(|error| CopyError {
|
||||||
|
src: src.clone(),
|
||||||
|
dst: dst.as_ref().into(),
|
||||||
|
error,
|
||||||
|
})?;
|
||||||
|
match copy_dir_raw(src, dst.as_ref()) {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
Err(err) => {
|
||||||
|
_ = std::fs::remove_dir_all(dst.as_ref());
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_dir_raw(src: PathBuf, dst: &Path) -> Result<(), CopyError> {
|
||||||
|
let global_err = |error| CopyError {
|
||||||
|
src: src.clone(),
|
||||||
|
dst: dst.into(),
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
let src_tree = TreeDir::new(src.clone()).map_err(global_err)?;
|
||||||
|
for ent in src_tree {
|
||||||
|
let ent = ent.map_err(global_err)?;
|
||||||
|
let src_full_path = src.join(&ent.relpath);
|
||||||
|
let dst_full_path = dst.join(&ent.relpath);
|
||||||
|
|
||||||
|
copy_fs_node(&src_full_path, &dst_full_path).map_err(|error| CopyError {
|
||||||
|
src: src_full_path.clone(),
|
||||||
|
dst: dst_full_path.clone(),
|
||||||
|
error,
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_fs_node(src: &Path, dst: &Path) -> std::io::Result<()> {
|
||||||
|
let metadata = std::fs::symlink_metadata(src)?;
|
||||||
|
|
||||||
|
if metadata.file_type().is_dir() {
|
||||||
|
std::fs::create_dir(dst)?;
|
||||||
|
} else if metadata.is_symlink() {
|
||||||
|
let link_to = std::fs::read_link(src)?;
|
||||||
|
symlink(&link_to, dst)?;
|
||||||
|
return Ok(());
|
||||||
|
} else {
|
||||||
|
std::fs::copy(&src, &dst)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::set_permissions(dst, metadata.permissions())?;
|
||||||
|
set_modified_time(dst, metadata.modified()?)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_family = "unix")]
|
||||||
|
pub fn symlink(src: &Path, dst: &Path) -> std::io::Result<()> {
|
||||||
|
std::os::unix::fs::symlink(src, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_family = "unix")]
|
||||||
|
fn set_modified_time(path: &Path, modified: SystemTime) -> std::io::Result<()> {
|
||||||
|
let modified_duration = modified
|
||||||
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
.map_err(|_| std::io::ErrorKind::InvalidData)?;
|
||||||
|
unsafe {
|
||||||
|
nix::sys::stat::utimensat(
|
||||||
|
std::os::fd::BorrowedFd::borrow_raw(nix::libc::AT_FDCWD),
|
||||||
|
path,
|
||||||
|
&nix::sys::time::TimeSpec::UTIME_NOW,
|
||||||
|
&nix::sys::time::TimeSpec::from_duration(modified_duration),
|
||||||
|
nix::sys::stat::UtimensatFlags::NoFollowSymlink,
|
||||||
|
)
|
||||||
|
.map_err(|errno| std::io::Error::from_raw_os_error(errno as _))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
#[error("{}", t!("common.CopyError", src = src.display(), dst = dst.display(), error = error))]
|
||||||
|
pub struct CopyError {
|
||||||
|
src: PathBuf,
|
||||||
|
dst: PathBuf,
|
||||||
|
error: std::io::Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_family = "unix")]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct LockGuard {
|
||||||
|
_inner: Option<nix::fcntl::Flock<std::fs::File>>,
|
||||||
|
}
|
||||||
|
#[cfg(target_family = "unix")]
|
||||||
|
impl LockGuard {
|
||||||
|
pub fn noop() -> Self {
|
||||||
|
Self { _inner: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open(path: impl AsRef<Path>, nonblocking: bool) -> std::io::Result<Self> {
|
||||||
|
let file = std::fs::File::options()
|
||||||
|
.create(true)
|
||||||
|
.write(true)
|
||||||
|
.open(path)?;
|
||||||
|
let flags = if nonblocking {
|
||||||
|
nix::fcntl::FlockArg::LockExclusiveNonblock
|
||||||
|
} else {
|
||||||
|
nix::fcntl::FlockArg::LockExclusive
|
||||||
|
};
|
||||||
|
let lock = nix::fcntl::Flock::lock(file, flags).map_err(|x| x.1)?;
|
||||||
|
Ok(Self { _inner: Some(lock) })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#[macro_export]
|
||||||
|
macro_rules! impl_serde_str {
|
||||||
|
($t:ty) => {
|
||||||
|
impl serde::Serialize for $t {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(&self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'de> serde::Deserialize<'de> for $t {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
String::deserialize(deserializer)?
|
||||||
|
.parse()
|
||||||
|
.map_err(|err| serde::de::Error::custom(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
pub mod checksum;
|
||||||
|
pub mod fs;
|
||||||
|
pub mod macros;
|
||||||
|
|
||||||
|
pub use fs::*;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
io::{Read, Seek, SeekFrom},
|
||||||
|
time::SystemTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn decompress(
|
||||||
|
mut raw: impl Read + Seek + Send + Sync + 'static,
|
||||||
|
) -> std::io::Result<Box<dyn Read + Send + Sync>> {
|
||||||
|
#[cfg(feature = "zstd")]
|
||||||
|
if stream_starts_with(&mut raw, &[0x28, 0xb5, 0x2f, 0xfd])? {
|
||||||
|
return Ok(Box::new(zstd::Decoder::new(raw)?));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "gzip")]
|
||||||
|
if stream_starts_with(&mut raw, &[0x1F, 0x8B])? {
|
||||||
|
return Ok(Box::new(flate2::read::GzDecoder::new(raw)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidData,
|
||||||
|
"Unknown compression algorithm",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stream_starts_with(
|
||||||
|
stream: &mut (impl Read + Seek + Send + Sync + 'static),
|
||||||
|
magic: &[u8],
|
||||||
|
) -> std::io::Result<bool> {
|
||||||
|
let mut magic_buf = vec![0u8; magic.len()];
|
||||||
|
stream.read_exact(&mut magic_buf)?;
|
||||||
|
stream.seek(SeekFrom::Start(0))?;
|
||||||
|
Ok(magic_buf == magic)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn timestamp_secs() -> i64 {
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_secs() as i64)
|
||||||
|
.unwrap_or_else(|e| -(e.duration().as_secs() as i64))
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
mod repo;
|
||||||
|
|
||||||
|
pub use repo::RepoEntry;
|
||||||
|
|
||||||
|
use rustc_hash::FxHashMap;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// A collection of all loaded Packie config files.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AllConfig {
|
||||||
|
pub repos: FxHashMap<String, RepoEntry>,
|
||||||
|
}
|
||||||
|
impl AllConfig {
|
||||||
|
pub fn open<P: AsRef<Path>>(path: P) -> Self {
|
||||||
|
let path = path.as_ref();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
repos: repo::read_repos(&path.join("repos.d")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
use rustc_hash::FxHashMap;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::Path;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct RepoConfig(FxHashMap<String, RepoEntry>);
|
||||||
|
impl RepoConfig {
|
||||||
|
pub fn read<P: AsRef<Path>>(path: P) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
|
let s = std::fs::read_to_string(path)?;
|
||||||
|
let result: Self = toml::from_str(&s)?;
|
||||||
|
for ent in result.0.values() {
|
||||||
|
if ent.url.is_some() && ent.urls.is_some() {
|
||||||
|
return Err(Box::from("cannot specify both `url` and `urls`"));
|
||||||
|
}
|
||||||
|
if ent.url.is_none() && ent.urls.is_none() {
|
||||||
|
return Err(Box::from("no url specified"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct RepoEntry {
|
||||||
|
url: Option<Url>,
|
||||||
|
urls: Option<Vec<Url>>,
|
||||||
|
}
|
||||||
|
impl RepoEntry {
|
||||||
|
pub fn urls(&self) -> &[Url] {
|
||||||
|
self.urls
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or_else(|| std::slice::from_ref(self.url.as_ref().unwrap()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_repos(repos_dir: &Path) -> FxHashMap<String, RepoEntry> {
|
||||||
|
let mut repos = FxHashMap::default();
|
||||||
|
if let Ok(tree_dir) = crate::common::TreeDir::new(repos_dir) {
|
||||||
|
for ent in tree_dir {
|
||||||
|
let Ok(ent) = ent else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !ent.ty.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Ok(repo_config) = RepoConfig::read(repos_dir.join(&ent.relpath)) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
for (key, val) in repo_config.0 {
|
||||||
|
repos.insert(key, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
repos
|
||||||
|
}
|
||||||
+219
@@ -0,0 +1,219 @@
|
|||||||
|
use crate::{
|
||||||
|
local::{InstallFlags, Installation},
|
||||||
|
package::{Package, PackageError, PkgIdent, PkgSpec},
|
||||||
|
repo::{RepoError, RepoPackage},
|
||||||
|
};
|
||||||
|
use rust_i18n::t;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct InstallOptions {
|
||||||
|
/// Package spec to record for this installation.
|
||||||
|
pub install_pkgspec: Option<PkgSpec>,
|
||||||
|
}
|
||||||
|
impl InstallOptions {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl super::Packie {
|
||||||
|
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.
|
||||||
|
pub fn install_package_file<P: AsRef<Path>>(
|
||||||
|
&mut self,
|
||||||
|
path: P,
|
||||||
|
options: &InstallOptions,
|
||||||
|
) -> Result<(), InstallError> {
|
||||||
|
let package = Package::open(path)?;
|
||||||
|
self.install_package(&package, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Installs a package, from a constructed [`Package`].
|
||||||
|
pub fn install_package(
|
||||||
|
&mut self,
|
||||||
|
package: &Package,
|
||||||
|
options: &InstallOptions,
|
||||||
|
) -> Result<(), InstallError> {
|
||||||
|
// Collect necessary information
|
||||||
|
let pkg_manifest = package.manifest()?;
|
||||||
|
let pkg_ident = pkg_manifest.pkg_ident();
|
||||||
|
let installed_size = package.installed_size()?;
|
||||||
|
let install_pkgspec = options
|
||||||
|
.install_pkgspec
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| default_install_pkgspec(pkg_ident.clone()));
|
||||||
|
|
||||||
|
// Check if the package is previously installed
|
||||||
|
if !self
|
||||||
|
.search_installation(&pkg_ident.clone().into())?
|
||||||
|
.is_empty()
|
||||||
|
{
|
||||||
|
return Err(InstallError::AlreadyInstalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the package spec is valid
|
||||||
|
if !install_pkgspec.matches(&pkg_ident) {
|
||||||
|
return Err(InstallError::PkgSpec);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy package files
|
||||||
|
let dest_dir = self.profile.pkg_dir.join(pkg_ident.prefix_name());
|
||||||
|
crate::common::copy_dir(package.bundle_dir(), dest_dir).map_err(InstallError::Copy)?;
|
||||||
|
|
||||||
|
// Record installation in the database
|
||||||
|
let installation = Installation {
|
||||||
|
pkg_manifest: pkg_manifest.clone(),
|
||||||
|
install_pkgspec,
|
||||||
|
install_date: crate::common::timestamp_secs(),
|
||||||
|
update_date: crate::common::timestamp_secs(),
|
||||||
|
installed_size,
|
||||||
|
install_flags: InstallFlags::default(),
|
||||||
|
};
|
||||||
|
self.local_db.insert_installation(&installation)?;
|
||||||
|
|
||||||
|
// Record abstract package provision in the database
|
||||||
|
for i in installation.pkg_manifest.provides.iter() {
|
||||||
|
self.local_db.insert_abspkg(&i, &pkg_ident)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy links file
|
||||||
|
if let Some(links) = package.links() {
|
||||||
|
std::fs::copy(&links.0, self.local_data.links_file(&pkg_ident))
|
||||||
|
.map_err(|err| InstallError::Database(Box::new(err)))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable default links
|
||||||
|
if let Some(links) = package.links() {
|
||||||
|
for linkdes in links.iter().ok().into_iter().flatten() {
|
||||||
|
let Ok(linkdes) = linkdes else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !linkdes.default {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_ = crate::link::activate_by_description(self, &pkg_ident, &linkdes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum InstallError {
|
||||||
|
#[error("{}", t!("install.InstallError.NotFound", item = .0))]
|
||||||
|
NotFound(String),
|
||||||
|
|
||||||
|
#[error("{}", t!("install.InstallError.DependencyRing"))]
|
||||||
|
DependencyRing,
|
||||||
|
|
||||||
|
#[error("{0}")]
|
||||||
|
Package(PackageError),
|
||||||
|
|
||||||
|
#[error("incompatible pkgspec")]
|
||||||
|
PkgSpec,
|
||||||
|
|
||||||
|
#[error("{0}")]
|
||||||
|
Copy(crate::common::CopyError),
|
||||||
|
|
||||||
|
#[error("{}", t!("install.InstallError.AlreadyInstalled"))]
|
||||||
|
AlreadyInstalled,
|
||||||
|
|
||||||
|
#[error("{0}")]
|
||||||
|
Repo(RepoError),
|
||||||
|
|
||||||
|
#[error("{}", t!("install.InstallError.Database", error = .0))]
|
||||||
|
Database(Box<dyn std::error::Error + Send + Sync>),
|
||||||
|
}
|
||||||
|
impl From<PackageError> for InstallError {
|
||||||
|
fn from(value: PackageError) -> Self {
|
||||||
|
Self::Package(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<rusqlite::Error> for InstallError {
|
||||||
|
fn from(value: rusqlite::Error) -> Self {
|
||||||
|
Self::Database(Box::new(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_install_pkgspec(pkg_ident: PkgIdent) -> PkgSpec {
|
||||||
|
PkgSpec {
|
||||||
|
name: pkg_ident.name,
|
||||||
|
version: crate::version::VersionFilter::Any,
|
||||||
|
arch: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
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.
|
||||||
|
fn prior_pkg(all: impl Iterator<Item = PkgIdent>, prior_arch: &str) -> PkgIdent {
|
||||||
|
let mut all = all.collect::<Vec<_>>();
|
||||||
|
all.sort_by(|x, y| x.version.cmp(&y.version));
|
||||||
|
all.reverse();
|
||||||
|
for i in &all {
|
||||||
|
if i.arch == prior_arch {
|
||||||
|
return i.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
all.remove(0)
|
||||||
|
}
|
||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
mod common;
|
||||||
|
pub mod config;
|
||||||
|
pub mod install;
|
||||||
|
pub mod link;
|
||||||
|
pub mod local;
|
||||||
|
pub mod package;
|
||||||
|
pub mod profile;
|
||||||
|
pub mod remove;
|
||||||
|
pub mod repo;
|
||||||
|
pub mod version;
|
||||||
|
|
||||||
|
use common::fs::LockGuard;
|
||||||
|
use config::AllConfig;
|
||||||
|
use local::{Cache, DataDir, LocalDb};
|
||||||
|
use profile::Profile;
|
||||||
|
use rust_i18n::t;
|
||||||
|
|
||||||
|
rust_i18n::i18n!("locales/libpackie", fallback = "en");
|
||||||
|
|
||||||
|
/// Main Packie state.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Packie {
|
||||||
|
profile: Profile,
|
||||||
|
local_db: LocalDb,
|
||||||
|
config: AllConfig,
|
||||||
|
local_data: DataDir,
|
||||||
|
cache: Cache,
|
||||||
|
_lock_guard: LockGuard,
|
||||||
|
}
|
||||||
|
impl Packie {
|
||||||
|
/// Returns profile of the Packie instance.
|
||||||
|
pub fn profile(&self) -> &Profile {
|
||||||
|
&self.profile
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns cache manager for the Packie instance.
|
||||||
|
pub fn cache(&self) -> &Cache {
|
||||||
|
&self.cache
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PackieBuilder {
|
||||||
|
profile: Profile,
|
||||||
|
readonly: bool,
|
||||||
|
nonblocking: bool,
|
||||||
|
}
|
||||||
|
impl PackieBuilder {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
profile: Profile::builtin(),
|
||||||
|
readonly: false,
|
||||||
|
nonblocking: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readonly(mut self, val: bool) -> Self {
|
||||||
|
self.readonly = val;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nonblocking(mut self, val: bool) -> Self {
|
||||||
|
self.nonblocking = val;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn profile(mut self, val: Profile) -> Self {
|
||||||
|
self.profile = val;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(self) -> Result<Packie, BuildPackieError> {
|
||||||
|
let mut _lock_guard = LockGuard::noop();
|
||||||
|
if !self.readonly {
|
||||||
|
_ = std::fs::create_dir_all(&self.profile.packie_data_dir);
|
||||||
|
_ = std::fs::create_dir_all(&self.profile.packie_cache_dir);
|
||||||
|
_ = std::fs::create_dir_all(&self.profile.packie_config_dir);
|
||||||
|
_ = std::fs::create_dir_all(&self.profile.pkg_dir);
|
||||||
|
|
||||||
|
let lock_path = self.profile.packie_data_dir.join("packie.lock");
|
||||||
|
_lock_guard =
|
||||||
|
LockGuard::open(lock_path, self.nonblocking).map_err(BuildPackieError::Lock)?;
|
||||||
|
}
|
||||||
|
let local_db_path = self.profile.packie_data_dir.join(LocalDb::FILENAME);
|
||||||
|
let open_local_db = if self.readonly {
|
||||||
|
LocalDb::open_ro
|
||||||
|
} else {
|
||||||
|
LocalDb::open_rw
|
||||||
|
};
|
||||||
|
let local_db = open_local_db(local_db_path).map_err(BuildPackieError::LocalDb)?;
|
||||||
|
let config = AllConfig::open(&self.profile.packie_config_dir);
|
||||||
|
let cache = Cache(self.profile.packie_cache_dir.clone());
|
||||||
|
let local_data = DataDir(self.profile.packie_data_dir.clone());
|
||||||
|
|
||||||
|
Ok(Packie {
|
||||||
|
profile: self.profile,
|
||||||
|
local_db,
|
||||||
|
config,
|
||||||
|
cache,
|
||||||
|
local_data,
|
||||||
|
_lock_guard,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum BuildPackieError {
|
||||||
|
#[error("{0}")]
|
||||||
|
LocalDb(rusqlite::Error),
|
||||||
|
|
||||||
|
#[error("{}", t!("BuildPackieError.Lock", error = .0))]
|
||||||
|
Lock(std::io::Error),
|
||||||
|
}
|
||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
use crate::{Packie, package::PkgIdent};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{
|
||||||
|
fs::File,
|
||||||
|
io::{BufRead, BufReader},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
impl super::Packie {
|
||||||
|
pub fn links_of_pkg(&self, pkgident: &PkgIdent) -> LinksFile {
|
||||||
|
LinksFile(self.local_data.links_file(pkgident))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterator over link descriptions in a links file.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct LinksFile(pub PathBuf);
|
||||||
|
impl LinksFile {
|
||||||
|
pub const FILENAME: &str = "_links";
|
||||||
|
|
||||||
|
pub fn iter(&self) -> std::io::Result<impl Iterator<Item = std::io::Result<LinkDescription>>> {
|
||||||
|
Ok(BufReader::new(File::open(&self.0)?)
|
||||||
|
.lines()
|
||||||
|
.map(|s| {
|
||||||
|
Ok::<_, std::io::Error>(
|
||||||
|
serde_json::from_str(&s?)
|
||||||
|
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.flatten())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Description of a link.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LinkDescription {
|
||||||
|
/// Path of the link source, relative to the package bundle.
|
||||||
|
pub from: String,
|
||||||
|
|
||||||
|
/// Path of the link destination, must be absolute.
|
||||||
|
pub to: String,
|
||||||
|
|
||||||
|
/// True if the link is enabled on installation, by default.
|
||||||
|
#[serde(default)]
|
||||||
|
pub default: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn activate_by_description(
|
||||||
|
packie: &mut Packie,
|
||||||
|
pkg_ident: &PkgIdent,
|
||||||
|
description: &LinkDescription,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
let bundle_path = packie.profile.pkg_dir.join(pkg_ident.prefix_name());
|
||||||
|
let from = bundle_path.join(&description.from);
|
||||||
|
create(from, &description.to)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deactivate_by_description(
|
||||||
|
packie: &mut Packie,
|
||||||
|
pkg_ident: &PkgIdent,
|
||||||
|
description: &LinkDescription,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
let bundle_path = packie.profile.pkg_dir.join(pkg_ident.prefix_name());
|
||||||
|
let from = bundle_path.join(&description.from);
|
||||||
|
let link_from = std::fs::read_link(&description.to)?;
|
||||||
|
if link_from != from {
|
||||||
|
return Err(std::io::ErrorKind::InvalidData.into());
|
||||||
|
}
|
||||||
|
remove(&description.to)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create(from: impl AsRef<Path>, to: impl AsRef<Path>) -> std::io::Result<()> {
|
||||||
|
std::fs::create_dir_all(
|
||||||
|
to.as_ref()
|
||||||
|
.parent()
|
||||||
|
.ok_or_else(|| std::io::ErrorKind::InvalidInput)?,
|
||||||
|
)?;
|
||||||
|
crate::common::symlink(from.as_ref(), to.as_ref())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tries to remove a link at a given path. Also removes all its parent directories if they become empty after
|
||||||
|
/// this deletion.
|
||||||
|
///
|
||||||
|
/// Note that this is not an atomic operation. Take care of TOCTOU and race condition.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// It would return an standard library IO error in given error kind:
|
||||||
|
///
|
||||||
|
/// - `NotFound`: The file could not be located.
|
||||||
|
/// - `InvalidData`: The file is not a link.
|
||||||
|
pub fn remove(path: impl Into<PathBuf>) -> std::io::Result<()> {
|
||||||
|
let mut path = path.into();
|
||||||
|
if !path.exists() {
|
||||||
|
return Err(std::io::ErrorKind::NotFound.into());
|
||||||
|
}
|
||||||
|
if std::fs::read_link(&path).is_err() {
|
||||||
|
return Err(std::io::ErrorKind::InvalidData.into());
|
||||||
|
}
|
||||||
|
std::fs::remove_file(&path)?;
|
||||||
|
while path.pop() {
|
||||||
|
_ = std::fs::remove_dir(&path);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
+304
@@ -0,0 +1,304 @@
|
|||||||
|
use crate::{
|
||||||
|
package::{AbsPkgIdent, PkgIdent, PkgManifest, PkgSpec},
|
||||||
|
version::VersionFilter,
|
||||||
|
};
|
||||||
|
use itertools::Itertools;
|
||||||
|
use rusqlite::{OpenFlags, Row, params};
|
||||||
|
use std::{
|
||||||
|
convert::Infallible,
|
||||||
|
fmt::Display,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl super::Packie {
|
||||||
|
pub fn search_installation(
|
||||||
|
&mut self,
|
||||||
|
pkgspec: &PkgSpec,
|
||||||
|
) -> rusqlite::Result<Vec<Installation>> {
|
||||||
|
let mut real = self
|
||||||
|
.local_db
|
||||||
|
.select_installation_by_name(&pkgspec.name)?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|x| pkgspec.matches(&x.pkg_manifest.pkg_ident()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if !matches!(pkgspec.version, VersionFilter::Any) {
|
||||||
|
return Ok(real);
|
||||||
|
}
|
||||||
|
let abspkg = AbsPkgIdent {
|
||||||
|
name: pkgspec.name.clone(),
|
||||||
|
arch: pkgspec.arch.clone(),
|
||||||
|
};
|
||||||
|
let abs = self.local_db.select_abspkg_by_ident(&abspkg)?;
|
||||||
|
for (_, provider) in abs {
|
||||||
|
real.push(self.local_db.select_installation_by_ident(&provider)?);
|
||||||
|
}
|
||||||
|
Ok(real
|
||||||
|
.into_iter()
|
||||||
|
.unique_by(|x| x.pkg_manifest.pkg_ident())
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn for_each_installation<E: From<rusqlite::Error>>(
|
||||||
|
&mut self,
|
||||||
|
f: impl FnMut(Installation) -> Result<(), E>,
|
||||||
|
) -> Result<(), E> {
|
||||||
|
self.local_db.for_each_installation(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DataDir(pub PathBuf);
|
||||||
|
impl DataDir {
|
||||||
|
const LINKS_FILES_DIR_NAME: &str = "links_files";
|
||||||
|
|
||||||
|
pub fn links_files_dir(&self) -> PathBuf {
|
||||||
|
self.dir(&[Self::LINKS_FILES_DIR_NAME])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn links_file(&self, name: &PkgIdent) -> PathBuf {
|
||||||
|
self.links_files_dir().join(name.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dir(&self, relative: &[&str]) -> PathBuf {
|
||||||
|
let mut path = self.0.clone();
|
||||||
|
for i in relative {
|
||||||
|
path.push(i);
|
||||||
|
}
|
||||||
|
_ = std::fs::create_dir_all(&path);
|
||||||
|
path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Cache(pub PathBuf);
|
||||||
|
impl Cache {
|
||||||
|
const REPO_DIR_NAME: &str = "repo";
|
||||||
|
|
||||||
|
pub fn repos_dir(&self) -> PathBuf {
|
||||||
|
self.dir(&[Self::REPO_DIR_NAME])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn repo_dir(&self, name: &str) -> PathBuf {
|
||||||
|
self.dir(&[Self::REPO_DIR_NAME, name])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dir(&self, relative: &[&str]) -> PathBuf {
|
||||||
|
let mut path = self.0.clone();
|
||||||
|
for i in relative {
|
||||||
|
path.push(i);
|
||||||
|
}
|
||||||
|
_ = std::fs::create_dir_all(&path);
|
||||||
|
path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct LocalDb(rusqlite::Connection);
|
||||||
|
impl LocalDb {
|
||||||
|
pub const FILENAME: &str = "local.db";
|
||||||
|
|
||||||
|
pub const SCHEMA: &str = r#"
|
||||||
|
CREATE TABLE IF NOT EXISTS "installation"(
|
||||||
|
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"pkg_name" TEXT NOT NULL,
|
||||||
|
"pkg_version" TEXT NOT NULL,
|
||||||
|
"pkg_arch" TEXT NOT NULL,
|
||||||
|
"pkg_manifest" TEXT NOT NULL,
|
||||||
|
"install_pkgspec" TEXT NOT NULL,
|
||||||
|
"install_date" INTEGER NOT NULL,
|
||||||
|
"update_date" INTEGER NOT NULL,
|
||||||
|
"installed_size" INTEGER NOT NULL,
|
||||||
|
"install_flags" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "abspkg"(
|
||||||
|
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"abspkg_ident" TEXT NOT NULL,
|
||||||
|
"provider" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
"#;
|
||||||
|
|
||||||
|
pub fn open_ro<P: AsRef<Path>>(path: P) -> rusqlite::Result<Self> {
|
||||||
|
rusqlite::Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY).map(Self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_rw<P: AsRef<Path>>(path: P) -> rusqlite::Result<Self> {
|
||||||
|
let conn = rusqlite::Connection::open(path)?;
|
||||||
|
conn.execute_batch(Self::SCHEMA)?;
|
||||||
|
Ok(Self(conn))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_installation(&mut self, installation: &Installation) -> rusqlite::Result<()> {
|
||||||
|
let pkg_manifest = serde_json::to_string(&installation.pkg_manifest)
|
||||||
|
.map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
|
||||||
|
self.0
|
||||||
|
.prepare_cached(
|
||||||
|
r#"
|
||||||
|
INSERT INTO "installation"(
|
||||||
|
"pkg_name", "pkg_version", "pkg_arch", "pkg_manifest",
|
||||||
|
"install_pkgspec", "install_date", "update_date", "installed_size",
|
||||||
|
"install_flags"
|
||||||
|
)
|
||||||
|
VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
|
||||||
|
"#,
|
||||||
|
)?
|
||||||
|
.execute(params![
|
||||||
|
installation.pkg_manifest.name,
|
||||||
|
installation.pkg_manifest.version.to_string(),
|
||||||
|
installation.pkg_manifest.arch,
|
||||||
|
pkg_manifest,
|
||||||
|
installation.install_pkgspec.to_string(),
|
||||||
|
installation.install_date,
|
||||||
|
installation.update_date,
|
||||||
|
installation.installed_size as i64,
|
||||||
|
installation.install_flags.to_string(),
|
||||||
|
])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_abspkg(
|
||||||
|
&mut self,
|
||||||
|
abspkg: &AbsPkgIdent,
|
||||||
|
provider: &PkgIdent,
|
||||||
|
) -> rusqlite::Result<()> {
|
||||||
|
self.0
|
||||||
|
.prepare_cached(r#"INSERT INTO "abspkg"("abspkg_ident", "provider") VALUES(?1, ?2)"#)?
|
||||||
|
.execute(params![abspkg.to_string(), provider.to_string()])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_installation_by_name(
|
||||||
|
&mut self,
|
||||||
|
pkg_name: &str,
|
||||||
|
) -> rusqlite::Result<Vec<Installation>> {
|
||||||
|
let mut stmt = self
|
||||||
|
.0
|
||||||
|
.prepare_cached(r#"SELECT * FROM "installation" WHERE "pkg_name" = ?1"#)?;
|
||||||
|
let mapped_rows = stmt.query_map(params![pkg_name], map_installation)?;
|
||||||
|
let mut ret = Vec::with_capacity(16);
|
||||||
|
for mr in mapped_rows {
|
||||||
|
ret.push(mr?);
|
||||||
|
}
|
||||||
|
Ok(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_installation_by_ident(
|
||||||
|
&mut self,
|
||||||
|
pkg_ident: &PkgIdent,
|
||||||
|
) -> rusqlite::Result<Installation> {
|
||||||
|
self.0.prepare_cached(r#"
|
||||||
|
SELECT * FROM "installation" WHERE "pkg_name" = ?1 AND "pkg_version" = ?2 AND "pkg_arch" = ?3
|
||||||
|
"#)?.query_one(params![pkg_ident.name, pkg_ident.version.to_string(), pkg_ident.arch], map_installation)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_abspkg_by_ident(
|
||||||
|
&mut self,
|
||||||
|
ident: &AbsPkgIdent,
|
||||||
|
) -> rusqlite::Result<Vec<(AbsPkgIdent, PkgIdent)>> {
|
||||||
|
let mut stmt = self
|
||||||
|
.0
|
||||||
|
.prepare_cached(r#"SELECT * FROM "abspkg" WHERE "abspkg_ident" = ?1"#)?;
|
||||||
|
let rows = stmt.query_map(params![ident.to_string()], map_abspkg)?;
|
||||||
|
let mut abspkg = Vec::new();
|
||||||
|
for i in rows {
|
||||||
|
abspkg.push(i?);
|
||||||
|
}
|
||||||
|
Ok(abspkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn for_each_installation<E: From<rusqlite::Error>>(
|
||||||
|
&mut self,
|
||||||
|
mut f: impl FnMut(Installation) -> Result<(), E>,
|
||||||
|
) -> Result<(), E> {
|
||||||
|
let mut stmt = self.0.prepare_cached("SELECT * FROM \"installation\"")?;
|
||||||
|
for i in stmt.query_map(params![], map_installation)? {
|
||||||
|
f(i?)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_installation(&mut self, pkg_ident: &PkgIdent) -> rusqlite::Result<bool> {
|
||||||
|
self
|
||||||
|
.0
|
||||||
|
.prepare_cached(r#"DELETE FROM "installation" WHERE "pkg_name" = ?1 AND "pkg_version" = ?2 AND "pkg_arch" = ?3"#)?
|
||||||
|
.execute(params![pkg_ident.name, pkg_ident.version.to_string(), pkg_ident.arch])
|
||||||
|
.map(|x| x > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_abspkg_provided_by(&mut self, provider: &PkgIdent) -> rusqlite::Result<bool> {
|
||||||
|
self.0
|
||||||
|
.prepare_cached(r#"DELETE FROM "abspkg" WHERE "provider" = ?1"#)?
|
||||||
|
.execute(params![provider.to_string()])
|
||||||
|
.map(|x| x > 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Installation {
|
||||||
|
pub pkg_manifest: PkgManifest,
|
||||||
|
pub install_pkgspec: PkgSpec,
|
||||||
|
pub install_date: i64,
|
||||||
|
pub update_date: i64,
|
||||||
|
pub installed_size: u64,
|
||||||
|
pub install_flags: InstallFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct InstallFlags(Vec<String>);
|
||||||
|
impl InstallFlags {
|
||||||
|
pub const AUTO_INSTALL: &str = "auto-install";
|
||||||
|
|
||||||
|
pub fn new(v: &[&str]) -> Self {
|
||||||
|
Self(v.iter().map(ToString::to_string).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contains(&self, s: &str) -> bool {
|
||||||
|
self.0.iter().any(|x| x == s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for InstallFlags {
|
||||||
|
type Err = Infallible;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Ok(Self(s.split(' ').map(Into::into).collect()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Display for InstallFlags {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
for i in self.0.iter() {
|
||||||
|
write!(f, "{i} ")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_installation(row: &Row) -> rusqlite::Result<Installation> {
|
||||||
|
Ok(Installation {
|
||||||
|
pkg_manifest: serde_json::from_str(&row.get::<_, String>("pkg_manifest")?)
|
||||||
|
.map_err(from_sql_error)?,
|
||||||
|
install_pkgspec: row
|
||||||
|
.get::<_, String>("install_pkgspec")?
|
||||||
|
.parse()
|
||||||
|
.map_err(from_sql_error)?,
|
||||||
|
install_date: row.get("install_date")?,
|
||||||
|
update_date: row.get("update_date")?,
|
||||||
|
installed_size: row.get::<_, i64>("installed_size")? as _,
|
||||||
|
install_flags: row.get::<_, String>("install_flags")?.parse().unwrap(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_abspkg(row: &Row) -> rusqlite::Result<(AbsPkgIdent, PkgIdent)> {
|
||||||
|
let provider = row
|
||||||
|
.get::<_, String>("provider")?
|
||||||
|
.parse()
|
||||||
|
.map_err(from_sql_error)?;
|
||||||
|
let abspkg_name = row
|
||||||
|
.get::<_, String>("abspkg_ident")?
|
||||||
|
.parse()
|
||||||
|
.map_err(from_sql_error)?;
|
||||||
|
Ok((abspkg_name, provider))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_sql_error<E: std::error::Error + Send + Sync + 'static>(error: E) -> rusqlite::Error {
|
||||||
|
rusqlite::Error::FromSqlConversionFailure(0, rusqlite::types::Type::Text, Box::new(error))
|
||||||
|
}
|
||||||
+335
@@ -0,0 +1,335 @@
|
|||||||
|
use crate::{
|
||||||
|
link::LinksFile,
|
||||||
|
version::{Version, VersionFilter},
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sha3::{Digest, Sha3_224};
|
||||||
|
use std::{
|
||||||
|
fmt::Display,
|
||||||
|
fs::File,
|
||||||
|
io::Read,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Package {
|
||||||
|
file_path: PathBuf,
|
||||||
|
extracted_dir: TempDir,
|
||||||
|
}
|
||||||
|
impl Package {
|
||||||
|
/// Opens a package file.
|
||||||
|
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, PackageError> {
|
||||||
|
let open_file = File::open(path.as_ref()).map_err(PackageError::Open)?;
|
||||||
|
let decompressed_file = crate::common::decompress(open_file).map_err(PackageError::Open)?;
|
||||||
|
let mut archive = tar::Archive::new(decompressed_file);
|
||||||
|
|
||||||
|
let extracted_dir = TempDir::new().map_err(PackageError::Open)?;
|
||||||
|
archive.unpack(&extracted_dir).map_err(PackageError::Open)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
file_path: path.as_ref().into(),
|
||||||
|
extracted_dir,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns [`PkgManifest`] for this package.
|
||||||
|
pub fn manifest(&self) -> Result<PkgManifest, PackageError> {
|
||||||
|
PkgManifest::read_at(self.extracted_dir.path().join(PkgManifest::FILENAME))
|
||||||
|
.map_err(PackageError::Manifest)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn links(&self) -> Option<LinksFile> {
|
||||||
|
let path = self.extracted_dir.path().join(LinksFile::FILENAME);
|
||||||
|
if path.exists() {
|
||||||
|
Some(LinksFile(path))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns path of the bundle directory of this package.
|
||||||
|
pub fn bundle_dir(&self) -> PathBuf {
|
||||||
|
self.extracted_dir.path().join("bundle")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns installed size of this package.
|
||||||
|
pub fn installed_size(&self) -> Result<u64, PackageError> {
|
||||||
|
crate::common::du_dir(self.bundle_dir()).map_err(PackageError::Bundle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Representation of the `PkgManifest.json` file.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct PkgManifest {
|
||||||
|
/// Package name.
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
/// Package version.
|
||||||
|
pub version: Version,
|
||||||
|
|
||||||
|
/// Package architecture.
|
||||||
|
pub arch: String,
|
||||||
|
|
||||||
|
/// Package description.
|
||||||
|
#[serde(default)]
|
||||||
|
pub description: String,
|
||||||
|
|
||||||
|
/// Package maintainers.
|
||||||
|
#[serde(default)]
|
||||||
|
pub maintainers: Vec<String>,
|
||||||
|
|
||||||
|
/// Package dependencies.
|
||||||
|
#[serde(default)]
|
||||||
|
pub dependencies: Vec<PkgSpec>,
|
||||||
|
|
||||||
|
/// Package recommended (soft) dependencies.
|
||||||
|
#[serde(default)]
|
||||||
|
pub recommendations: Vec<PkgSpec>,
|
||||||
|
|
||||||
|
/// Provided abstract packages.
|
||||||
|
#[serde(default)]
|
||||||
|
pub provides: Vec<AbsPkgIdent>,
|
||||||
|
}
|
||||||
|
impl PkgManifest {
|
||||||
|
pub const FILENAME: &str = "PkgManifest.json";
|
||||||
|
|
||||||
|
pub const SIZE_MAX: u64 = 2 * 1024 * 1024;
|
||||||
|
|
||||||
|
pub fn read_at<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
|
||||||
|
let mut data = Vec::with_capacity(1024);
|
||||||
|
let mut file = File::open(path)?.take(PkgManifest::SIZE_MAX);
|
||||||
|
file.read_to_end(&mut data)?;
|
||||||
|
serde_json::from_slice(&data)
|
||||||
|
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pkg_ident(&self) -> PkgIdent {
|
||||||
|
PkgIdent {
|
||||||
|
name: self.name.clone(),
|
||||||
|
version: self.version.clone(),
|
||||||
|
arch: self.arch.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const PKGNAME_FORBIDDEN_CHARS: &str = "~!@#$%^&*()={}[]\\|:;'\"<>,/?\r\n ";
|
||||||
|
|
||||||
|
/// A package specifier.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PkgSpec {
|
||||||
|
pub name: String,
|
||||||
|
pub version: VersionFilter,
|
||||||
|
pub arch: Option<String>,
|
||||||
|
}
|
||||||
|
impl PkgSpec {
|
||||||
|
pub fn matches(&self, pkg_ident: &PkgIdent) -> bool {
|
||||||
|
self.name == pkg_ident.name
|
||||||
|
&& self.version.matches(&pkg_ident.version)
|
||||||
|
&& match self.arch.as_deref() {
|
||||||
|
Some(arch) => pkg_ident.arch == arch,
|
||||||
|
None => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<PkgIdent> for PkgSpec {
|
||||||
|
fn from(value: PkgIdent) -> Self {
|
||||||
|
Self {
|
||||||
|
name: value.name,
|
||||||
|
version: VersionFilter::Eq(value.version),
|
||||||
|
arch: Some(value.arch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for PkgSpec {
|
||||||
|
type Err = ParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if let Ok(pkgident) = PkgIdent::from_str(s) {
|
||||||
|
return Ok(Self {
|
||||||
|
name: pkgident.name,
|
||||||
|
version: VersionFilter::Eq(pkgident.version),
|
||||||
|
arch: Some(pkgident.arch),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let s = s.trim();
|
||||||
|
let mut name = String::new();
|
||||||
|
let mut version = None;
|
||||||
|
let mut arch = None;
|
||||||
|
let mut chars = s.chars().peekable();
|
||||||
|
|
||||||
|
while let Some(&c) = chars.peek() {
|
||||||
|
if c == '(' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if c == '@' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
name.push(chars.next().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
if chars.peek() == Some(&'(') {
|
||||||
|
chars.next();
|
||||||
|
let mut ver = String::new();
|
||||||
|
while let Some(&c) = chars.peek() {
|
||||||
|
if c == ')' {
|
||||||
|
chars.next();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ver.push(chars.next().unwrap());
|
||||||
|
}
|
||||||
|
version = Some(ver);
|
||||||
|
|
||||||
|
while chars.peek() == Some(&' ') {
|
||||||
|
chars.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if chars.peek() == Some(&'@') {
|
||||||
|
chars.next();
|
||||||
|
let mut arch_str = String::new();
|
||||||
|
while chars.peek().is_some() {
|
||||||
|
arch_str.push(chars.next().unwrap());
|
||||||
|
}
|
||||||
|
arch = Some(arch_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = name.trim_end().to_string();
|
||||||
|
if name.chars().any(|ch| PKGNAME_FORBIDDEN_CHARS.contains(|x| x == ch)) {
|
||||||
|
return Err(ParseError::Pkgname);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
name,
|
||||||
|
version: version.as_deref().unwrap_or("*").parse()?,
|
||||||
|
arch,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Display for PkgSpec {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.name)?;
|
||||||
|
if self.version != VersionFilter::Any {
|
||||||
|
write!(f, " ({})", self.version)?;
|
||||||
|
}
|
||||||
|
if let Some(arch) = self.arch.as_ref() {
|
||||||
|
write!(f, " @{}", arch)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crate::impl_serde_str!(PkgSpec);
|
||||||
|
|
||||||
|
/// A package identifier that specifies a unique package.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct PkgIdent {
|
||||||
|
pub name: String,
|
||||||
|
pub version: Version,
|
||||||
|
pub arch: String,
|
||||||
|
}
|
||||||
|
impl PkgIdent {
|
||||||
|
pub fn prefix_name(&self) -> String {
|
||||||
|
let mut hasher = Sha3_224::new();
|
||||||
|
hasher.update(self.to_string().as_bytes());
|
||||||
|
let hash = const_hex::encode(hasher.finalize());
|
||||||
|
format!("{}-{}", self.name, hash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Display for PkgIdent {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}={}@{}", self.name, self.version, self.arch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for PkgIdent {
|
||||||
|
type Err = ParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let mut name = String::new();
|
||||||
|
let mut version = String::new();
|
||||||
|
let mut arch = String::new();
|
||||||
|
let mut handling = "name";
|
||||||
|
for c in s.chars() {
|
||||||
|
if c == '=' {
|
||||||
|
handling = "version";
|
||||||
|
} else if c == '@' {
|
||||||
|
handling = "arch";
|
||||||
|
} else if handling == "name" {
|
||||||
|
name.push(c);
|
||||||
|
} else if handling == "version" {
|
||||||
|
version.push(c);
|
||||||
|
} else if handling == "arch" {
|
||||||
|
arch.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = name.trim().to_string();
|
||||||
|
if name.chars().any(|ch| PKGNAME_FORBIDDEN_CHARS.contains(|x| x == ch)) {
|
||||||
|
return Err(ParseError::Pkgname);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
name,
|
||||||
|
version: version.trim().parse()?,
|
||||||
|
arch: arch.trim().into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crate::impl_serde_str!(PkgIdent);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct AbsPkgIdent {
|
||||||
|
pub name: String,
|
||||||
|
pub arch: Option<String>,
|
||||||
|
}
|
||||||
|
impl Display for AbsPkgIdent {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match &self.arch {
|
||||||
|
Some(arch) => write!(f, "{}@{}", self.name, arch),
|
||||||
|
None => write!(f, "{}", self.name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for AbsPkgIdent {
|
||||||
|
type Err = std::convert::Infallible;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let (name, arch) = s
|
||||||
|
.split_once('@')
|
||||||
|
.map(|(a, b)| (a, Some(b)))
|
||||||
|
.unwrap_or((s, None));
|
||||||
|
Ok(Self {
|
||||||
|
name: name.into(),
|
||||||
|
arch: arch.map(Into::into),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crate::impl_serde_str!(AbsPkgIdent);
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum ParseError {
|
||||||
|
#[error("invalid package name")]
|
||||||
|
Pkgname,
|
||||||
|
|
||||||
|
#[error("{0}")]
|
||||||
|
Version(crate::version::VersionError),
|
||||||
|
}
|
||||||
|
impl From<crate::version::VersionError> for ParseError {
|
||||||
|
fn from(error: crate::version::VersionError) -> Self {
|
||||||
|
Self::Version(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An error caused by dealing with package files.
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum PackageError {
|
||||||
|
#[error("failed to open package file: {0}")]
|
||||||
|
Open(std::io::Error),
|
||||||
|
|
||||||
|
#[error("failed to read package manifest: {0}")]
|
||||||
|
Manifest(std::io::Error),
|
||||||
|
|
||||||
|
#[error("failed to read package bundle: {0}")]
|
||||||
|
Bundle(std::io::Error),
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Profile {
|
||||||
|
/// Directory for storing package bundles, e.g. `/pkg`.
|
||||||
|
pub pkg_dir: PathBuf,
|
||||||
|
|
||||||
|
/// Packie data directory, e.g. `/var/db/packie`.
|
||||||
|
pub packie_data_dir: PathBuf,
|
||||||
|
|
||||||
|
/// Packie cache directory, e.g. `/var/cache/packie`.
|
||||||
|
pub packie_cache_dir: PathBuf,
|
||||||
|
|
||||||
|
/// Packie config directory, e.g. `/var/config/packie`.
|
||||||
|
pub packie_config_dir: PathBuf,
|
||||||
|
|
||||||
|
/// Host architecture, e.g. `riscv64-semios-linux`.
|
||||||
|
pub host_arch: String,
|
||||||
|
}
|
||||||
|
impl Profile {
|
||||||
|
pub fn builtin() -> Self {
|
||||||
|
Self {
|
||||||
|
pkg_dir: option_env!("PACKIE_PKG_DIR").unwrap_or("/pkg").into(),
|
||||||
|
packie_data_dir: option_env!("PACKIE_DATA_DIR")
|
||||||
|
.unwrap_or("/var/lib/packie")
|
||||||
|
.into(),
|
||||||
|
packie_cache_dir: option_env!("PACKIE_CACHE_DIR")
|
||||||
|
.unwrap_or("/var/cache/packie")
|
||||||
|
.into(),
|
||||||
|
packie_config_dir: option_env!("PACKIE_CONFIG_DIR")
|
||||||
|
.unwrap_or("/var/config/packie")
|
||||||
|
.into(),
|
||||||
|
host_arch: option_env!("PACKIE_HOST_ARCH")
|
||||||
|
.unwrap_or(&format!(
|
||||||
|
"{}-semios-{}",
|
||||||
|
std::env::consts::ARCH,
|
||||||
|
std::env::consts::OS
|
||||||
|
))
|
||||||
|
.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
use crate::{link::LinksFile, package::PkgSpec};
|
||||||
|
use rust_i18n::t;
|
||||||
|
|
||||||
|
impl super::Packie {
|
||||||
|
pub fn remove(&mut self, pkgspec: &PkgSpec) -> Result<(), RemoveError> {
|
||||||
|
// Find the installation to remove
|
||||||
|
let mut found = self
|
||||||
|
.search_installation(pkgspec)
|
||||||
|
.map_err(RemoveError::Database)?;
|
||||||
|
if found.is_empty() {
|
||||||
|
return Err(RemoveError::NotFound);
|
||||||
|
}
|
||||||
|
if found.len() != 1 {
|
||||||
|
return Err(RemoveError::NotUniquePkgSpec);
|
||||||
|
}
|
||||||
|
assert_eq!(found.len(), 1);
|
||||||
|
let found = found.remove(0);
|
||||||
|
let pkg_ident = found.pkg_manifest.pkg_ident();
|
||||||
|
|
||||||
|
// Remove all links
|
||||||
|
let links = LinksFile(self.local_data.links_file(&pkg_ident));
|
||||||
|
if let Ok(links) = links.iter() {
|
||||||
|
for link in links {
|
||||||
|
let Ok(link) = link else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
_ = crate::link::deactivate_by_description(self, &pkg_ident, &link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove package files
|
||||||
|
let bundle_dir = self.profile.pkg_dir.join(pkg_ident.prefix_name());
|
||||||
|
std::fs::remove_dir_all(&bundle_dir).map_err(RemoveError::RemoveFiles)?;
|
||||||
|
|
||||||
|
// Remove database records
|
||||||
|
self.local_db
|
||||||
|
.remove_installation(&pkg_ident)
|
||||||
|
.map_err(RemoveError::Database)?;
|
||||||
|
self.local_db
|
||||||
|
.remove_abspkg_provided_by(&pkg_ident)
|
||||||
|
.map_err(RemoveError::Database)?;
|
||||||
|
|
||||||
|
// Remove the links file
|
||||||
|
_ = std::fs::remove_file(self.local_data.links_file(&pkg_ident));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum RemoveError {
|
||||||
|
#[error("{}", t!("remove.RemoveError.NotFound"))]
|
||||||
|
NotFound,
|
||||||
|
|
||||||
|
#[error("{}", t!("remove.RemoveError.NotUniquePkgSpec"))]
|
||||||
|
NotUniquePkgSpec,
|
||||||
|
|
||||||
|
#[error("{}", t!("remove.RemoveError.RemoveFiles", error = .0))]
|
||||||
|
RemoveFiles(std::io::Error),
|
||||||
|
|
||||||
|
#[error("{}", t!("remove.RemoveError.Database", error = .0))]
|
||||||
|
Database(rusqlite::Error),
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
use rust_i18n::t;
|
||||||
|
use std::{
|
||||||
|
fs::File,
|
||||||
|
io::{Read, Seek, Write},
|
||||||
|
path::PathBuf,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Download<P> {
|
||||||
|
from: String,
|
||||||
|
to: PathBuf,
|
||||||
|
on_progress: P,
|
||||||
|
no_existence_check: bool,
|
||||||
|
checksum_required: Option<String>,
|
||||||
|
}
|
||||||
|
impl Download<fn(u64, u64)> {
|
||||||
|
pub fn new(from: String, to: PathBuf) -> Self {
|
||||||
|
Self {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
no_existence_check: false,
|
||||||
|
on_progress: |_, _| (),
|
||||||
|
checksum_required: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<P> Download<P> {
|
||||||
|
pub fn on_progress<P2>(self, on_progress: P2) -> Download<P2> {
|
||||||
|
Download {
|
||||||
|
from: self.from,
|
||||||
|
to: self.to,
|
||||||
|
no_existence_check: self.no_existence_check,
|
||||||
|
on_progress,
|
||||||
|
checksum_required: self.checksum_required,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn no_existence_check(mut self) -> Self {
|
||||||
|
self.no_existence_check = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn checksum_required(mut self, val: String) -> Self {
|
||||||
|
self.checksum_required = Some(val);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<P: FnMut(u64, u64)> Download<P> {
|
||||||
|
pub fn run(&mut self) -> Result<(), DownloadError> {
|
||||||
|
if self.to.exists() && !self.no_existence_check {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut temp_file_path = self.to.clone();
|
||||||
|
temp_file_path.add_extension("tmp");
|
||||||
|
let mut temp_file = File::options()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(&temp_file_path)
|
||||||
|
.map_err(DownloadError::Filesystem)?;
|
||||||
|
let seek = temp_file
|
||||||
|
.seek(std::io::SeekFrom::Current(0))
|
||||||
|
.map_err(DownloadError::Filesystem)?;
|
||||||
|
|
||||||
|
let mut resp = ureq::get(&self.from)
|
||||||
|
.header(ureq::http::header::RANGE, format!("bytes={seek}-"))
|
||||||
|
.call()
|
||||||
|
.map_err(|e| DownloadError::Http(Box::new(e)))?;
|
||||||
|
let body = resp.body_mut();
|
||||||
|
let total = body.content_length().unwrap_or_default() + seek;
|
||||||
|
let mut sum = seek;
|
||||||
|
|
||||||
|
let mut buf = [0; 1024];
|
||||||
|
let mut body_reader = body.as_reader();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let len = body_reader
|
||||||
|
.read(&mut buf)
|
||||||
|
.map_err(|e| DownloadError::Http(Box::new(e)))?;
|
||||||
|
if len == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
temp_file
|
||||||
|
.write_all(&buf[..len])
|
||||||
|
.map_err(DownloadError::Filesystem)?;
|
||||||
|
sum += len as u64;
|
||||||
|
(self.on_progress)(sum, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(expected) = self.checksum_required.as_ref() {
|
||||||
|
let verified =
|
||||||
|
crate::common::checksum::verify_file(&temp_file_path, expected).unwrap_or_default();
|
||||||
|
if !verified {
|
||||||
|
_ = std::fs::remove_file(&temp_file_path);
|
||||||
|
return Err(DownloadError::Checksum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::rename(temp_file_path, &self.to).map_err(DownloadError::Filesystem)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum DownloadError {
|
||||||
|
#[error("INVALID_URL")]
|
||||||
|
InvalidUrl,
|
||||||
|
|
||||||
|
#[error("{0}")]
|
||||||
|
Http(Box<dyn std::error::Error + Send + Sync>),
|
||||||
|
|
||||||
|
#[error("{0}")]
|
||||||
|
Filesystem(std::io::Error),
|
||||||
|
|
||||||
|
#[error("{}", t!("repo.download.DownloadError.Checksum"))]
|
||||||
|
Checksum,
|
||||||
|
}
|
||||||
+424
@@ -0,0 +1,424 @@
|
|||||||
|
mod download;
|
||||||
|
mod serve;
|
||||||
|
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use download::DownloadError;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use serve::RepoServeDir;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Packie,
|
||||||
|
package::{AbsPkgIdent, PkgIdent, PkgManifest, PkgSpec},
|
||||||
|
};
|
||||||
|
use download::Download;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use rusqlite::{OpenFlags, Row, params};
|
||||||
|
use rust_i18n::t;
|
||||||
|
use rustc_hash::FxHashMap;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
impl super::Packie {
|
||||||
|
/// Creates a session for syncing repositories.
|
||||||
|
pub fn sync_repo(&mut self) -> SyncSession<'_> {
|
||||||
|
SyncSession::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a session for querying repositories.
|
||||||
|
pub fn query_repo(&mut self) -> QuerySession {
|
||||||
|
QuerySession::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Download a package.
|
||||||
|
pub fn download_package(
|
||||||
|
&self,
|
||||||
|
repo: &str,
|
||||||
|
pkg_ident: &PkgIdent,
|
||||||
|
mut on_event: impl FnMut(DownloadPackageEvent),
|
||||||
|
) -> Result<PathBuf, RepoError> {
|
||||||
|
let repo_cache_dir = self.cache.repo_dir(repo);
|
||||||
|
|
||||||
|
let repo = self.config.repos.get(repo).ok_or(RepoError::NoSuchRepo)?;
|
||||||
|
let repo_db = repo_cache_dir.join(RepoDb::FILENAME);
|
||||||
|
let mut repo_db = RepoDb::open_ro(repo_db).map_err(RepoError::Database)?;
|
||||||
|
let repo_package = repo_db
|
||||||
|
.select_package_by_ident(pkg_ident)
|
||||||
|
.map_err(|_| RepoError::NoSuchPackage)?;
|
||||||
|
|
||||||
|
let standard_filename = format!("{pkg_ident}.pkg");
|
||||||
|
|
||||||
|
let download_path = repo_cache_dir.join(&standard_filename);
|
||||||
|
|
||||||
|
for repo_url in repo.urls() {
|
||||||
|
let download_url = match repo_url.join(&standard_filename) {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(_) => {
|
||||||
|
on_event(DownloadPackageEvent::Error(
|
||||||
|
repo_url.clone(),
|
||||||
|
RepoError::Download(DownloadError::InvalidUrl),
|
||||||
|
));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let download_result = Download::new(download_url.to_string(), download_path.clone())
|
||||||
|
.on_progress(|sum, total| {
|
||||||
|
on_event(DownloadPackageEvent::Progress(repo_url.clone(), sum, total))
|
||||||
|
})
|
||||||
|
.checksum_required(repo_package.checksum.clone())
|
||||||
|
.run();
|
||||||
|
if let Err(err) = download_result {
|
||||||
|
on_event(DownloadPackageEvent::Error(
|
||||||
|
download_url.clone(),
|
||||||
|
RepoError::Download(err),
|
||||||
|
));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return Ok(download_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(RepoError::AllTriesFailed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RepoDb(rusqlite::Connection);
|
||||||
|
impl RepoDb {
|
||||||
|
const FILENAME: &str = "repo.db";
|
||||||
|
|
||||||
|
const SCHEMA: &str = r#"
|
||||||
|
CREATE TABLE IF NOT EXISTS "package"(
|
||||||
|
"pkg_name" TEXT NOT NULL,
|
||||||
|
"pkg_version" TEXT NOT NULL,
|
||||||
|
"pkg_arch" TEXT NOT NULL,
|
||||||
|
"pkg_manifest" TEXT NOT NULL,
|
||||||
|
"download_size" INTEGER NOT NULL,
|
||||||
|
"installed_size" INTEGER NOT NULL,
|
||||||
|
"checksum" TEXT NOT NULL,
|
||||||
|
PRIMARY KEY ("pkg_name", "pkg_version", "pkg_arch")
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "abspkg"(
|
||||||
|
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"abspkg_ident" TEXT NOT NULL,
|
||||||
|
"provider" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
"#;
|
||||||
|
|
||||||
|
pub fn open_rw<P: AsRef<Path>>(path: P) -> rusqlite::Result<Self> {
|
||||||
|
let conn = rusqlite::Connection::open(path)?;
|
||||||
|
conn.execute_batch(Self::SCHEMA)?;
|
||||||
|
Ok(Self(conn))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_ro<P: AsRef<Path>>(path: P) -> rusqlite::Result<Self> {
|
||||||
|
rusqlite::Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY).map(Self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_package_by_ident(
|
||||||
|
&mut self,
|
||||||
|
pkg_ident: &PkgIdent,
|
||||||
|
) -> rusqlite::Result<RepoPackage> {
|
||||||
|
self.0.prepare_cached(
|
||||||
|
r#"SELECT * FROM "package" WHERE "pkg_name" = ?1 AND "pkg_version" = ?2 AND "pkg_arch" = ?3"#,
|
||||||
|
)?.query_one(params![pkg_ident.name, pkg_ident.version.to_string(), pkg_ident.arch], map_repo_package)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_package_by_name(&mut self, pkgname: &str) -> rusqlite::Result<Vec<RepoPackage>> {
|
||||||
|
let mut stmt = self
|
||||||
|
.0
|
||||||
|
.prepare_cached(r#"SELECT * FROM "package" WHERE "pkg_name" = ?1"#)?;
|
||||||
|
let mapped_rows = stmt.query_map(params![pkgname], map_repo_package)?;
|
||||||
|
let mut ret = Vec::with_capacity(16);
|
||||||
|
for mr in mapped_rows {
|
||||||
|
ret.push(mr?);
|
||||||
|
}
|
||||||
|
Ok(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_abspkg_by_ident(
|
||||||
|
&mut self,
|
||||||
|
ident: &AbsPkgIdent,
|
||||||
|
) -> rusqlite::Result<Vec<(AbsPkgIdent, PkgIdent)>> {
|
||||||
|
let mut stmt = self
|
||||||
|
.0
|
||||||
|
.prepare_cached(r#"SELECT * FROM "abspkg" WHERE "abspkg_ident" = ?1"#)?;
|
||||||
|
let rows = stmt.query_map(params![ident.to_string()], map_abspkg)?;
|
||||||
|
let mut ret = Vec::with_capacity(16);
|
||||||
|
for row in rows {
|
||||||
|
ret.push(row?);
|
||||||
|
}
|
||||||
|
Ok(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn for_each_package<E: From<rusqlite::Error>>(
|
||||||
|
&mut self,
|
||||||
|
mut f: impl FnMut(RepoPackage) -> Result<(), E>,
|
||||||
|
) -> Result<(), E> {
|
||||||
|
let mut stmt = self.0.prepare_cached("SELECT * FROM \"package\"")?;
|
||||||
|
let mapped_rows = stmt.query_map(params![], map_repo_package)?;
|
||||||
|
for mr in mapped_rows {
|
||||||
|
f(mr?)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_package(&mut self, package: &RepoPackage) -> rusqlite::Result<()> {
|
||||||
|
let pkg_manifest = serde_json::to_string(&package.pkg_manifest)
|
||||||
|
.map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
|
||||||
|
self.0
|
||||||
|
.prepare_cached(
|
||||||
|
r#"
|
||||||
|
INSERT INTO "package"(
|
||||||
|
"pkg_name", "pkg_version", "pkg_arch", "pkg_manifest",
|
||||||
|
"download_size", "installed_size", "checksum"
|
||||||
|
)
|
||||||
|
VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7)
|
||||||
|
"#,
|
||||||
|
)?
|
||||||
|
.execute(params![
|
||||||
|
package.pkg_manifest.name,
|
||||||
|
package.pkg_manifest.version.to_string(),
|
||||||
|
package.pkg_manifest.arch,
|
||||||
|
pkg_manifest,
|
||||||
|
package.download_size as i64,
|
||||||
|
package.installed_size as i64,
|
||||||
|
package.checksum
|
||||||
|
])
|
||||||
|
.map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_abspkg(&mut self, abs: &AbsPkgIdent, real: &PkgIdent) -> rusqlite::Result<()> {
|
||||||
|
self.0
|
||||||
|
.prepare_cached(r#"INSERT INTO "abspkg"("abspkg_ident", "provider") VALUES(?1, ?2)"#)?
|
||||||
|
.execute(params![abs.to_string(), real.to_string()])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_package_by_ident(&mut self, ident: &PkgIdent) -> rusqlite::Result<bool> {
|
||||||
|
self.0
|
||||||
|
.prepare_cached(
|
||||||
|
r#"DELETE FROM "package" WHERE "pkg_name" = ?1 AND "pkg_version" = ?2 AND "pkg_arch" = ?3"#,
|
||||||
|
)?
|
||||||
|
.execute(params![ident.name, ident.version.to_string(), ident.arch])
|
||||||
|
.map(|x| x > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_abspkg_by_provider(&mut self, ident: &PkgIdent) -> rusqlite::Result<bool> {
|
||||||
|
self.0
|
||||||
|
.prepare_cached(r#"DELETE FROM "abspkg" WHERE "provider" = ?1"#)?
|
||||||
|
.execute(params![ident.to_string()])
|
||||||
|
.map(|x| x > 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_repo_package(row: &Row) -> rusqlite::Result<RepoPackage> {
|
||||||
|
let pkg_manifest: String = row.get("pkg_manifest")?;
|
||||||
|
let pkg_manifest = serde_json::from_str(&pkg_manifest).map_err(from_sql_error)?;
|
||||||
|
Ok(RepoPackage {
|
||||||
|
pkg_manifest,
|
||||||
|
download_size: row.get::<_, i64>("download_size")? as u64,
|
||||||
|
installed_size: row.get::<_, i64>("installed_size")? as u64,
|
||||||
|
checksum: row.get("checksum")?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_abspkg(row: &Row) -> rusqlite::Result<(AbsPkgIdent, PkgIdent)> {
|
||||||
|
let provider = row
|
||||||
|
.get::<_, String>("provider")?
|
||||||
|
.parse()
|
||||||
|
.map_err(from_sql_error)?;
|
||||||
|
let abspkg_name = row
|
||||||
|
.get::<_, String>("abspkg_ident")?
|
||||||
|
.parse()
|
||||||
|
.map_err(from_sql_error)?;
|
||||||
|
Ok((abspkg_name, provider))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_sql_error<E: std::error::Error + Send + Sync + 'static>(error: E) -> rusqlite::Error {
|
||||||
|
rusqlite::Error::FromSqlConversionFailure(0, rusqlite::types::Type::Text, Box::new(error))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A record representing to a package in a repository.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct RepoPackage {
|
||||||
|
/// Package manifest.
|
||||||
|
pub pkg_manifest: PkgManifest,
|
||||||
|
|
||||||
|
/// Download size of the package.
|
||||||
|
pub download_size: u64,
|
||||||
|
|
||||||
|
/// Installed size of the package.
|
||||||
|
pub installed_size: u64,
|
||||||
|
|
||||||
|
/// Checksum of the package file.
|
||||||
|
pub checksum: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Session of syncing repositories.
|
||||||
|
pub struct SyncSession<'a, E = fn(SyncEvent)> {
|
||||||
|
packie: &'a mut Packie,
|
||||||
|
repo: Option<String>,
|
||||||
|
on_event: E,
|
||||||
|
}
|
||||||
|
impl<'a> SyncSession<'a> {
|
||||||
|
pub fn new(packie: &'a mut Packie) -> Self {
|
||||||
|
Self {
|
||||||
|
packie,
|
||||||
|
repo: None,
|
||||||
|
on_event: |_| (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a, E> SyncSession<'a, E> {
|
||||||
|
pub fn repo(mut self, repo: String) -> Self {
|
||||||
|
self.repo = Some(repo);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_event<E2>(self, on_event: E2) -> SyncSession<'a, E2> {
|
||||||
|
SyncSession {
|
||||||
|
packie: self.packie,
|
||||||
|
repo: self.repo,
|
||||||
|
on_event,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<E: FnMut(SyncEvent)> SyncSession<'_, E> {
|
||||||
|
pub fn run(mut self) {
|
||||||
|
let repos = match self.repo.clone() {
|
||||||
|
Some(x) => vec![x],
|
||||||
|
None => self.packie.config.repos.keys().cloned().collect(),
|
||||||
|
};
|
||||||
|
for repo_name in repos {
|
||||||
|
let Some(repo) = self.packie.config.repos.get(&repo_name).cloned() else {
|
||||||
|
self.raise_error(Some(repo_name), None, RepoError::NoSuchRepo);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
(self.on_event)(SyncEvent::BeginRepo(repo_name.clone()));
|
||||||
|
for url in repo.urls() {
|
||||||
|
match self.sync_url(&repo_name, &url) {
|
||||||
|
Ok(()) => break,
|
||||||
|
Err(err) => self.raise_error(Some(repo_name.clone()), Some(url.clone()), err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync_url(&mut self, repo_name: &str, url: &Url) -> Result<(), RepoError> {
|
||||||
|
let repo_db_url = url
|
||||||
|
.join(RepoDb::FILENAME)
|
||||||
|
.map_err(|_| DownloadError::InvalidUrl)
|
||||||
|
.map_err(RepoError::Download)?;
|
||||||
|
|
||||||
|
let repo_cache_dir = self.packie.cache.repo_dir(repo_name);
|
||||||
|
Download::new(
|
||||||
|
repo_db_url.to_string(),
|
||||||
|
repo_cache_dir.join(RepoDb::FILENAME),
|
||||||
|
)
|
||||||
|
.no_existence_check()
|
||||||
|
.on_progress(Box::new(|sum, total| {
|
||||||
|
self.raise_progress(repo_name.into(), repo_db_url.clone(), sum, total)
|
||||||
|
}))
|
||||||
|
.run()
|
||||||
|
.map_err(RepoError::Download)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raise_error(&mut self, repo: Option<String>, url: Option<Url>, error: RepoError) {
|
||||||
|
(self.on_event)(SyncEvent::Error(repo, url, error));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raise_progress(&mut self, repo: String, url: Url, downloaded_bytes: u64, total_bytes: u64) {
|
||||||
|
(self.on_event)(SyncEvent::Progress(SyncProgress {
|
||||||
|
repo: Some(repo),
|
||||||
|
url: Some(url),
|
||||||
|
downloaded_bytes,
|
||||||
|
total_bytes,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An event during running [`SyncSession`], which may report progress changes or errors.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum SyncEvent {
|
||||||
|
BeginRepo(String),
|
||||||
|
Progress(SyncProgress),
|
||||||
|
Error(Option<String>, Option<Url>, RepoError),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A progress while syncing repositories.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SyncProgress {
|
||||||
|
pub repo: Option<String>,
|
||||||
|
pub url: Option<Url>,
|
||||||
|
pub downloaded_bytes: u64,
|
||||||
|
pub total_bytes: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An event occurred while downloading a package.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum DownloadPackageEvent {
|
||||||
|
Progress(Url, u64, u64),
|
||||||
|
Error(Url, RepoError),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct QuerySession {
|
||||||
|
repos: FxHashMap<String, RepoDb>,
|
||||||
|
}
|
||||||
|
impl QuerySession {
|
||||||
|
pub fn new(packie: &mut Packie) -> Self {
|
||||||
|
let mut repos = FxHashMap::default();
|
||||||
|
for name in packie.config.repos.keys() {
|
||||||
|
let repo_db = packie.cache.repo_dir(name).join(RepoDb::FILENAME);
|
||||||
|
let Ok(repo_db) = RepoDb::open_ro(repo_db) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
repos.insert(name.into(), repo_db);
|
||||||
|
}
|
||||||
|
Self { repos }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search_package(&mut self, pkgspec: &PkgSpec) -> Vec<(String, RepoPackage)> {
|
||||||
|
let mut all: Vec<(String, RepoPackage)> = Vec::new();
|
||||||
|
for (name, db) in self.repos.iter_mut() {
|
||||||
|
for pkg in db.select_package_by_name(&pkgspec.name).unwrap_or_default() {
|
||||||
|
if pkgspec.matches(&pkg.pkg_manifest.pkg_ident()) {
|
||||||
|
all.push((name.into(), pkg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let abspkg_ident = AbsPkgIdent {
|
||||||
|
name: pkgspec.name.clone(),
|
||||||
|
arch: pkgspec.arch.clone(),
|
||||||
|
};
|
||||||
|
for (_, provider) in db.select_abspkg_by_ident(&abspkg_ident).unwrap_or_default() {
|
||||||
|
let Ok(real) = db.select_package_by_ident(&provider) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
all.push((name.into(), real));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
all.into_iter()
|
||||||
|
.unique_by(|(repo, pkg)| (repo.to_string(), pkg.pkg_manifest.pkg_ident()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum RepoError {
|
||||||
|
#[error("{}", t!("repo.RepoError.NoSuchRepo"))]
|
||||||
|
NoSuchRepo,
|
||||||
|
|
||||||
|
#[error("{}", t!("repo.RepoError.NoSuchPackage"))]
|
||||||
|
NoSuchPackage,
|
||||||
|
|
||||||
|
#[error("{}", t!("repo.RepoError.Database", error = 0))]
|
||||||
|
Database(rusqlite::Error),
|
||||||
|
|
||||||
|
#[error("{0}")]
|
||||||
|
Download(DownloadError),
|
||||||
|
|
||||||
|
#[error("{}", t!("repo.RepoError.AllTriesFailed"))]
|
||||||
|
AllTriesFailed,
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
use crate::{
|
||||||
|
common::checksum::DEFAULT_HASHER,
|
||||||
|
package::{Package, PkgIdent},
|
||||||
|
repo::{RepoDb, RepoPackage, download::Download},
|
||||||
|
};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
/// A helper type to deal with repository serve directories.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RepoServeDir {
|
||||||
|
path: PathBuf,
|
||||||
|
db: RepoDb,
|
||||||
|
}
|
||||||
|
impl RepoServeDir {
|
||||||
|
/// Open a directory.
|
||||||
|
pub fn open<P: Into<PathBuf>>(path: P) -> rusqlite::Result<Self> {
|
||||||
|
let path = path.into();
|
||||||
|
Ok(Self {
|
||||||
|
db: RepoDb::open_rw(path.join(RepoDb::FILENAME))?,
|
||||||
|
path,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clone<P: Into<PathBuf>>(
|
||||||
|
url: &Url,
|
||||||
|
path: P,
|
||||||
|
mut on_event: impl FnMut(Option<String>, Url, u64, u64),
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
let path = path.into();
|
||||||
|
std::fs::create_dir_all(&path)?;
|
||||||
|
|
||||||
|
let repo_db_url = url.join(RepoDb::FILENAME)?;
|
||||||
|
Download::new(repo_db_url.to_string(), path.join(RepoDb::FILENAME))
|
||||||
|
.on_progress(|sum, total| on_event(None, repo_db_url.clone(), sum, total))
|
||||||
|
.run()?;
|
||||||
|
|
||||||
|
let mut db = RepoDb::open_ro(path.join(RepoDb::FILENAME))?;
|
||||||
|
db.for_each_package::<Box<dyn std::error::Error + Send + Sync>>(|pkg| {
|
||||||
|
let pkg_ident = pkg.pkg_manifest.pkg_ident();
|
||||||
|
let standard_filename = format!("{}.pkg", pkg_ident.to_string());
|
||||||
|
let pkgfile_url = url.join(&standard_filename)?;
|
||||||
|
let pkgfile_path = path.join(&standard_filename);
|
||||||
|
if crate::common::checksum::verify_file(&pkgfile_path, &pkg.checksum)
|
||||||
|
.unwrap_or_default()
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Download::new(pkgfile_url.to_string(), pkgfile_path)
|
||||||
|
.on_progress(|sum, total| {
|
||||||
|
on_event(Some(pkg_ident.to_string()), pkgfile_url.clone(), sum, total)
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
.map_err::<Box<dyn std::error::Error + Send + Sync>, _>(|e| {
|
||||||
|
Box::from(format!("failed to download \"{pkgfile_url}\": {e}"))
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a package to the repository.
|
||||||
|
pub fn add_package<P: AsRef<Path>>(
|
||||||
|
&mut self,
|
||||||
|
path: P,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
let package = Package::open(path)?;
|
||||||
|
|
||||||
|
let pkg_manifest = package.manifest()?;
|
||||||
|
let download_size = std::fs::metadata(path)?.len();
|
||||||
|
let installed_size = package.installed_size()?;
|
||||||
|
let checksum = crate::common::checksum::hash_file(DEFAULT_HASHER, path)?;
|
||||||
|
|
||||||
|
let repo_package = RepoPackage {
|
||||||
|
pkg_manifest,
|
||||||
|
download_size,
|
||||||
|
installed_size,
|
||||||
|
checksum,
|
||||||
|
};
|
||||||
|
|
||||||
|
let standard_filename =
|
||||||
|
format!("{}.pkg", repo_package.pkg_manifest.pkg_ident().to_string());
|
||||||
|
std::fs::copy(path, self.path.join(standard_filename))?;
|
||||||
|
|
||||||
|
self.db.insert_package(&repo_package)?;
|
||||||
|
for abs in repo_package.pkg_manifest.provides.iter() {
|
||||||
|
self.db
|
||||||
|
.insert_abspkg(abs, &repo_package.pkg_manifest.pkg_ident())?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_package(
|
||||||
|
&mut self,
|
||||||
|
ident: &PkgIdent,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
let path = self.path.join(format!("{ident}.pkg"));
|
||||||
|
std::fs::remove_file(path)?;
|
||||||
|
self.db.delete_package_by_ident(ident)?;
|
||||||
|
self.db.delete_abspkg_by_provider(ident)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
+265
@@ -0,0 +1,265 @@
|
|||||||
|
//! 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 {
|
||||||
|
/// Version namespace. By default, the value is `0`. The value needs to be changed if the versioning
|
||||||
|
/// rule has an incompatible change.
|
||||||
|
pub namespace: u32,
|
||||||
|
|
||||||
|
/// Main version. This is usually determined by the upstream.
|
||||||
|
pub main: Vec<u32>,
|
||||||
|
|
||||||
|
/// Pre-release version suffix. This is usually determined by the upstream.
|
||||||
|
pub prerelease: Option<Prerelease>,
|
||||||
|
|
||||||
|
/// Meta version.
|
||||||
|
pub meta: Option<String>,
|
||||||
|
}
|
||||||
|
impl FromStr for Version {
|
||||||
|
type Err = VersionError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let (namespace, s) = match s.split_once(':') {
|
||||||
|
Some((a, b)) => (a.parse().map_err(|_| VersionError::ParseNamespace)?, b),
|
||||||
|
None => (0, s),
|
||||||
|
};
|
||||||
|
let (s, meta) = match s.split_once('+') {
|
||||||
|
Some((a, b)) => (a, Some(b.into())),
|
||||||
|
None => (s, None),
|
||||||
|
};
|
||||||
|
let (s, prerelease) = match s.split_once('-') {
|
||||||
|
Some((a, b)) => (a, Some(b.parse()?)),
|
||||||
|
None => (s, None),
|
||||||
|
};
|
||||||
|
let mut main = Vec::new();
|
||||||
|
for i in s.split('.') {
|
||||||
|
main.push(i.parse().map_err(|_| VersionError::ParseMain)?);
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
namespace,
|
||||||
|
main,
|
||||||
|
prerelease,
|
||||||
|
meta,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl PartialOrd for Version {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
let namespace = self.namespace.cmp(&other.namespace);
|
||||||
|
if namespace != Ordering::Equal {
|
||||||
|
return Some(namespace);
|
||||||
|
}
|
||||||
|
let main = self.main.cmp(&other.main);
|
||||||
|
if main != Ordering::Equal {
|
||||||
|
return Some(main);
|
||||||
|
}
|
||||||
|
let prerelease = match (&self.prerelease, &other.prerelease) {
|
||||||
|
(None, None) => Ordering::Equal,
|
||||||
|
(None, Some(_)) => Ordering::Greater,
|
||||||
|
(Some(_), None) => Ordering::Less,
|
||||||
|
(Some(a), Some(b)) => a.cmp(b),
|
||||||
|
};
|
||||||
|
if prerelease != Ordering::Equal {
|
||||||
|
return Some(prerelease);
|
||||||
|
}
|
||||||
|
match (&self.meta, &other.meta) {
|
||||||
|
(None, None) => Some(Ordering::Equal),
|
||||||
|
(None, Some(_)) => Some(Ordering::Less),
|
||||||
|
(Some(_), None) => Some(Ordering::Greater),
|
||||||
|
(Some(a), Some(b)) => Some(a.cmp(&b)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Ord for Version {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
self.partial_cmp(other).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Display for Version {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if self.namespace != 0 {
|
||||||
|
write!(f, "{}:", self.namespace)?;
|
||||||
|
}
|
||||||
|
let main = self
|
||||||
|
.main
|
||||||
|
.iter()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(".");
|
||||||
|
write!(f, "{main}",)?;
|
||||||
|
if let Some(prerelease) = &self.prerelease {
|
||||||
|
write!(f, "-{prerelease}")?;
|
||||||
|
}
|
||||||
|
if let Some(meta) = &self.meta {
|
||||||
|
write!(f, "+{meta}")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crate::impl_serde_str!(Version);
|
||||||
|
impl Version {}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct Prerelease {
|
||||||
|
pub tag: String,
|
||||||
|
pub number: u32,
|
||||||
|
}
|
||||||
|
impl FromStr for Prerelease {
|
||||||
|
type Err = VersionError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let Some((tag, number)) = s.split_once('.') else {
|
||||||
|
return Err(VersionError::ParsePrerelease);
|
||||||
|
};
|
||||||
|
Ok(Self {
|
||||||
|
tag: tag.into(),
|
||||||
|
number: number.parse().map_err(|_| VersionError::ParsePrerelease)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Display for Prerelease {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}.{}", self.tag, self.number)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
|
pub enum VersionFilter {
|
||||||
|
/// The '*' filter, matching any versions.
|
||||||
|
#[default]
|
||||||
|
Any,
|
||||||
|
|
||||||
|
/// The '..., ..., ...' filter.
|
||||||
|
All(Vec<VersionFilter>),
|
||||||
|
|
||||||
|
/// The '<=' filter.
|
||||||
|
Lte(Version),
|
||||||
|
|
||||||
|
/// The '>=' filter.
|
||||||
|
Gte(Version),
|
||||||
|
|
||||||
|
/// The '=' filter.
|
||||||
|
Eq(Version),
|
||||||
|
|
||||||
|
/// The '~' (or default) filter.
|
||||||
|
Compatible(Version),
|
||||||
|
|
||||||
|
/// The '+' filter.
|
||||||
|
WithMeta(String),
|
||||||
|
}
|
||||||
|
impl VersionFilter {
|
||||||
|
pub fn matches(&self, version: &Version) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Any => true,
|
||||||
|
Self::Lte(other) => version <= other,
|
||||||
|
Self::Gte(other) => version >= other,
|
||||||
|
Self::Eq(other) => version == other,
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for VersionFilter {
|
||||||
|
type Err = VersionError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
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()?));
|
||||||
|
}
|
||||||
|
if let Some(version) = s.strip_prefix(">=") {
|
||||||
|
return Ok(Self::Gte(version.parse()?));
|
||||||
|
}
|
||||||
|
if let Some(version) = s.strip_prefix("<=") {
|
||||||
|
return Ok(Self::Lte(version.parse()?));
|
||||||
|
}
|
||||||
|
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()?));
|
||||||
|
}
|
||||||
|
return Ok(Self::Compatible(s.parse()?));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Display for VersionFilter {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Any => write!(f, "*"),
|
||||||
|
Self::Lte(ver) => write!(f, "<={ver}"),
|
||||||
|
Self::Gte(ver) => write!(f, ">={ver}"),
|
||||||
|
Self::Eq(ver) => write!(f, "={ver}"),
|
||||||
|
Self::All(cond) => write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
cond.iter()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
),
|
||||||
|
Self::Compatible(ver) => write!(f, "~{ver}"),
|
||||||
|
Self::WithMeta(meta) => write!(f, "+{meta}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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("{}", t!("version.VersionError.ParseNamespace"))]
|
||||||
|
ParseNamespace,
|
||||||
|
|
||||||
|
#[error("{}", t!("version.VersionError.ParseMain"))]
|
||||||
|
ParseMain,
|
||||||
|
|
||||||
|
#[error("{}", t!("version.VersionError.ParsePrerelease"))]
|
||||||
|
ParsePrerelease,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn semver_is_compatible(a: &Version, b: &Version) -> bool {
|
||||||
|
if a.namespace != b.namespace {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if a.main.first().copied() != b.main.first().copied() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cmp_prerelease() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
assert!(Prerelease::from_str("rc.1")? > Prerelease::from_str("beta.2")?);
|
||||||
|
assert!(Prerelease::from_str("beta.1")? > Prerelease::from_str("alpha.1")?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cmp_version() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
assert!(Version::from_str("100:1.0.0")? > Version::from_str("99:1.0.0")?);
|
||||||
|
assert!(Version::from_str("1:1.0.0")? > Version::from_str("1.0.0")?);
|
||||||
|
assert!(Version::from_str("0:1.0.0")? == Version::from_str("1.0.0")?);
|
||||||
|
assert!(Version::from_str("1.0.0-rc.1")? < Version::from_str("1.0.0")?);
|
||||||
|
assert!(Version::from_str("1.0.0-rc.1")? > Version::from_str("0.5.1")?);
|
||||||
|
assert!(Version::from_str("1.0.0-rc.1+1")? > Version::from_str("1.0.0-rc.1")?);
|
||||||
|
assert!(Version::from_str("1.0.0+1")? > Version::from_str("1.0.0")?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user