initial commit

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