Initial commit

Signed-off-by: sisungo <[email protected]>
This commit is contained in:
2026-06-10 00:33:24 +08:00
commit e6fa0df9a5
22 changed files with 1378 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
/target
/Cargo.lock
/.zed
/.idea
/.vscode
.DS_Store
._.DS_Store
+25
View File
@@ -0,0 +1,25 @@
[package]
name = "packie"
version = "0.1.0"
edition = "2024"
[features]
default = ["zstd", "gzip"]
zstd = ["dep:zstd"]
gzip = ["dep:flate2"]
[workspace]
members = ["cli"]
[dependencies]
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"
tar = "0.4"
tempfile = "3"
thiserror = "2"
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.
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "packie-cli"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "packie"
path = "src/main.rs"
[dependencies]
anyhow = "1"
clap = { version = "4", features = ["derive"] }
packie = { path = "../" }
rust-i18n = "4"
+28
View File
@@ -0,0 +1,28 @@
use anyhow::anyhow;
use clap::Parser;
use packie::{PackieBuilder, package::PkgSpec};
#[derive(Debug, Parser)]
pub struct Cli {
pkgspec: PkgSpec,
}
pub fn main(cli: Cli) -> anyhow::Result<()> {
let mut packie = PackieBuilder::new().readonly(true).build()?;
let mut found = packie::installation::by_pkgspec(&mut packie, &cli.pkgspec)?;
if found.len() == 0 {
return Err(anyhow!("no package found"));
}
if found.len() != 1 {
return Err(anyhow!("not unique package spec"));
}
let found = found.remove(0);
println!("Package name: {}", found.pkg_manifest.name);
println!("Package version: {}", found.pkg_manifest.version);
println!("Package architecture: {}", found.pkg_manifest.arch);
println!("Install package spec: {}", found.install_pkgspec);
println!("Installed size: {}", found.installed_size);
Ok(())
}
+10
View File
@@ -0,0 +1,10 @@
use clap::Parser;
use packie::PackieBuilder;
#[derive(Debug, Parser)]
pub struct Cli {}
pub fn main(cli: Cli) -> anyhow::Result<()> {
PackieBuilder::new().build()?;
Ok(())
}
+49
View File
@@ -0,0 +1,49 @@
use clap::Parser;
use packie::{PackieBuilder, install::InstallOptions, package::PkgSpec};
use std::{
path::{Path, PathBuf},
str::FromStr,
};
#[derive(Debug, Parser)]
pub struct Cli {
items: Vec<Item>,
}
#[derive(Debug, Clone)]
enum Item {
LocalFile(PathBuf),
PkgSpec(PkgSpec),
}
impl FromStr for Item {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if Path::new(s).exists() {
Ok(Self::LocalFile(s.into()))
} else {
Ok(Self::PkgSpec(s.parse()?))
}
}
}
pub fn main(cli: Cli) -> anyhow::Result<()> {
let mut packie = PackieBuilder::new().build()?;
for i in cli.items {
match i {
Item::LocalFile(path) => {
packie::install::install_package_file(
&mut packie,
path,
&InstallOptions {
install_pkgspec: None,
},
)?;
}
Item::PkgSpec(pkgspec) => {
todo!()
}
}
}
Ok(())
}
+48
View File
@@ -0,0 +1,48 @@
mod info;
mod initdb;
mod install;
mod print;
mod remove;
use clap::Parser;
use rust_i18n::t;
rust_i18n::i18n!("../locales/packie-cli");
#[derive(Debug, Parser)]
enum 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),
}
fn main() {
rust_i18n::set_locale(match std::env::var("LANG").as_deref() {
Ok("zh_CN.UTF-8") => "zh-CN",
_ => "en",
});
let cli = Cli::parse();
let result = match cli {
Cli::Info(cli) => info::main(cli),
Cli::InitDb(cli) => initdb::main(cli),
Cli::Install(cli) => install::main(cli),
Cli::Print(cli) => print::main(cli),
Cli::Remove(cli) => remove::main(cli),
};
if let Err(err) = result {
eprintln!("{}", t!("main.error", error = err));
std::process::exit(1);
}
}
+19
View File
@@ -0,0 +1,19 @@
use anyhow::anyhow;
use clap::Parser;
use packie::PackieBuilder;
#[derive(Debug, Parser)]
pub struct Cli {
key: String,
}
pub fn main(cli: Cli) -> anyhow::Result<()> {
let packie = PackieBuilder::new().readonly(true).build()?;
match &cli.key[..] {
"profile.host_arch" => {
println!("{}", packie.profile().host_arch);
Ok(())
}
unknown => Err(anyhow!("unknown key {unknown} to print")),
}
}
+17
View File
@@ -0,0 +1,17 @@
use anyhow::anyhow;
use clap::Parser;
use packie::{PackieBuilder, package::PkgSpec};
#[derive(Debug, Parser)]
pub struct Cli {
pkgspecs: Vec<PkgSpec>,
}
pub fn main(cli: Cli) -> anyhow::Result<()> {
let mut packie = PackieBuilder::new().build()?;
for pkgspec in cli.pkgspecs {
packie::remove::remove(&mut packie, &pkgspec)
.map_err(|err| anyhow!("failed to remove {pkgspec}: {err}"))?;
}
Ok(())
}
+22
View File
@@ -0,0 +1,22 @@
_version: 2
common.CopyError:
en: 'failed to copy from "%{src}" to "%{dst}": %{error}'
zh-CN: '无法将 "%{src}" 复制到 "%{dst}"%{error}'
install.InstallError.Database:
en: 'local database error: %{error}'
zh-CN: '本地数据库错误:%{error}'
remove.RemoveError.NotUniquePkgSpec:
en: 'not unique package spec'
zh-CN: '指定的包范围匹配到了多个包'
remove.RemoveError.Database:
en: 'local database error: %{error}'
zh-CN: '本地数据库错误:%{error}'
remove.RemoveError.NotFound:
en: 'package not found'
zh-CN: '找不到包'
remove.RemoveError.RemoveFiles:
en: 'failed to remove package files: %{error}'
zh-CN: '无法删除包文件:%{error}'
install.InstallError.AlreadyInstalled:
en: 'package already installed'
zh-CN: '此包已经安装'
+4
View File
@@ -0,0 +1,4 @@
_version: 2
main.error:
en: 'error: %{error}'
zh-CN: '错误:%{error}'
+206
View File
@@ -0,0 +1,206 @@
use rust_i18n::t;
use std::{
fs::{FileType, ReadDir},
io::{Read, Seek, SeekFrom},
path::{Path, PathBuf},
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)
}
#[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> {
let mut size = 0;
let path = path.as_ref();
for ent in TreeDir::new(path)? {
let ent = ent?;
let metadata = std::fs::symlink_metadata(path.join(&ent.relpath))?;
size += metadata.len();
}
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")]
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,
}
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))
}
pub fn timestamp_millis() -> i64 {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or_else(|e| -(e.duration().as_millis() as i64))
}
+103
View File
@@ -0,0 +1,103 @@
use crate::{
Packie,
installation::Installation,
package::{Package, PackageError, PkgIdent, PkgSpec},
};
use rust_i18n::t;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct InstallOptions {
pub install_pkgspec: Option<PkgSpec>,
}
/// Installs a package, from a local package file.
pub fn install_package_file<P: AsRef<Path>>(
packie: &mut Packie,
path: P,
options: &InstallOptions,
) -> Result<(), InstallError> {
let package = Package::open(path)?;
install_package(packie, &package, options)
}
/// Installs a package, from a constructed [`Package`].
pub fn install_package(
packie: &mut Packie,
package: &Package,
options: &InstallOptions,
) -> Result<(), InstallError> {
// Collect necessary information
let pkg_manifest = package.manifest()?;
let installed_size = package.installed_size()?;
let install_pkgspec = options
.install_pkgspec
.clone()
.unwrap_or_else(|| default_install_pkgspec(pkg_manifest.pkg_ident()));
// Check if the package is previously installed
if !crate::installation::by_pkgspec(packie, &pkg_manifest.pkg_ident().into())
.map_err(InstallError::Database)?
.is_empty()
{
return Err(InstallError::AlreadyInstalled);
}
// Check if the package spec is valid
if !install_pkgspec.matches(&pkg_manifest.pkg_ident()) {
return Err(InstallError::PkgSpec);
}
// Copy package files
let dest_dir = packie
.profile
.pkg_dir
.join(pkg_manifest.pkg_ident().to_string());
crate::common::copy_dir(package.bundle_dir(), dest_dir).map_err(InstallError::Copy)?;
// Record installation in the database
let installation = Installation {
pkg_manifest,
install_pkgspec,
install_date: crate::common::timestamp_secs(),
update_date: crate::common::timestamp_secs(),
installed_size,
};
packie
.local_db
.insert_installation(&installation)
.map_err(InstallError::Database)?;
Ok(())
}
#[derive(Debug, thiserror::Error)]
pub enum InstallError {
#[error("{0}")]
Package(PackageError),
#[error("incompatible pkgspec")]
PkgSpec,
#[error("{0}")]
Copy(crate::common::CopyError),
#[error("{}", t!("install.InstallError.AlreadyInstalled"))]
AlreadyInstalled,
#[error("{}", t!("install.InstallError.Database", error = 0))]
Database(rusqlite::Error),
}
impl From<PackageError> for InstallError {
fn from(value: PackageError) -> Self {
Self::Package(value)
}
}
fn default_install_pkgspec(pkg_ident: PkgIdent) -> PkgSpec {
PkgSpec {
name: pkg_ident.name,
version: crate::version::VersionFilter::Any,
arch: None,
}
}
+22
View File
@@ -0,0 +1,22 @@
use crate::{
Packie,
package::{PkgManifest, PkgSpec},
};
#[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 fn by_pkgspec(packie: &mut Packie, pkgspec: &PkgSpec) -> rusqlite::Result<Vec<Installation>> {
Ok(packie
.local_db
.select_installation_by_pkgname(&pkgspec.name)?
.into_iter()
.filter(|x| pkgspec.matches(&x.pkg_manifest.pkg_ident()))
.collect())
}
+77
View File
@@ -0,0 +1,77 @@
mod common;
pub mod install;
pub mod installation;
pub mod link;
pub mod local_db;
pub mod package;
pub mod profile;
pub mod remove;
pub mod version;
use local_db::LocalDb;
use profile::Profile;
rust_i18n::i18n!("locales/libpackie", fallback = "en");
/// Main Packie state.
#[derive(Debug)]
pub struct Packie {
profile: Profile,
local_db: LocalDb,
}
impl Packie {
/// Returns profile of the Packie instance.
pub fn profile(&self) -> &Profile {
&self.profile
}
}
#[derive(Debug)]
pub struct PackieBuilder {
profile: Profile,
readonly: bool,
}
impl PackieBuilder {
pub fn new() -> Self {
Self {
profile: Profile::builtin(),
readonly: false,
}
}
pub fn readonly(mut self, val: bool) -> Self {
self.readonly = val;
self
}
pub fn profile(mut self, val: Profile) -> Self {
self.profile = val;
self
}
pub fn build(self) -> Result<Packie, BuildPackieError> {
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 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)?;
Ok(Packie {
profile: self.profile,
local_db,
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum BuildPackieError {
#[error("{0}")]
LocalDb(rusqlite::Error),
}
+28
View File
@@ -0,0 +1,28 @@
use serde::{Deserialize, Serialize};
/// 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,
}
/// Status of a link in an installed package.
#[derive(Debug, Clone)]
pub struct LinkStatus {
/// Absolute path of the link source.
pub from: String,
/// Absolute path of the link destination.
pub to: String,
/// True if the link is activated.
pub activated: bool,
}
+104
View File
@@ -0,0 +1,104 @@
use crate::{installation::Installation, package::PkgIdent};
use rusqlite::{OpenFlags, Row, params};
use std::path::Path;
/// Schema of the database.
///
/// The "installation" table records installed packages.
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
);
"#;
#[derive(Debug)]
pub struct LocalDb(rusqlite::Connection);
impl LocalDb {
pub const FILENAME: &str = "local.db";
pub fn open_ro<P: AsRef<Path>>(path: P) -> rusqlite::Result<Self> {
let conn = rusqlite::Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY)?;
Ok(Self(conn))
}
pub fn open_rw<P: AsRef<Path>>(path: P) -> rusqlite::Result<Self> {
let conn = rusqlite::Connection::open(path)?;
conn.execute_batch(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"
)
VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
"#,
)?
.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,
])?;
Ok(())
}
pub fn select_installation_by_pkgname(
&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 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)
}
}
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 _,
})
}
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))
}
+216
View File
@@ -0,0 +1,216 @@
use crate::{
link::LinkDescription,
version::{Version, VersionFilter},
};
use serde::{Deserialize, Serialize};
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)
}
/// 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)
}
}
/// 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),
}
/// 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 dependencies.
#[serde(default)]
pub dependencies: Vec<PkgSpec>,
/// Links.
#[serde(default)]
pub links: Vec<LinkDescription>,
}
impl PkgManifest {
pub const FILENAME: &str = "PkgManifest.json";
pub const SIZE_MAX: u64 = 64 * 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(),
}
}
}
/// A package specifier.
#[derive(Debug, Clone, Serialize, Deserialize)]
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 = crate::version::VersionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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 let Some(&c) = chars.peek() {
arch_str.push(chars.next().unwrap());
}
arch = Some(arch_str);
}
let name = name.trim_end().to_string();
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(())
}
}
/// A package identifier that specifies a unique package.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PkgIdent {
pub name: String,
pub version: Version,
pub arch: String,
}
impl Display for PkgIdent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}={}@{}", self.name, self.version, self.arch)
}
}
+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/db/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(),
}
}
}
+46
View File
@@ -0,0 +1,46 @@
use crate::{Packie, package::PkgSpec};
use rust_i18n::t;
pub fn remove(packie: &mut Packie, pkgspec: &PkgSpec) -> Result<(), RemoveError> {
// Find the installation to remove
let mut found =
crate::installation::by_pkgspec(packie, 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);
// Remove package files
let bundle_dir = packie
.profile
.pkg_dir
.join(found.pkg_manifest.pkg_ident().to_string());
std::fs::remove_dir_all(&bundle_dir).map_err(RemoveError::RemoveFiles)?;
// Remove database records
packie
.local_db
.remove_installation(&found.pkg_manifest.pkg_ident())
.map_err(RemoveError::Database)?;
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),
}
+277
View File
@@ -0,0 +1,277 @@
//! Parsing and matching of software versions.
use std::{cmp::Ordering, fmt::Display, str::FromStr};
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))
}
}
};
}
#[derive(Debug, Clone, PartialEq, Eq)]
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 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(())
}
}
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.
Lte(Version),
/// The '>=' filter.
Gte(Version),
/// The '=' filter.
Eq(Version),
/// The '>=xxx, <=xxx' filter.
Between(Version, Version),
/// The '~' (or default) filter.
Compatible(Version),
}
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::Between(a, b) => version >= a && version <= b,
Self::Compatible(other) => semver_is_compatible(version, other),
}
}
}
impl FromStr for VersionFilter {
type Err = VersionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "*" {
return Ok(Self::Any);
}
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((a, b)) = s.split_once(',') {
let (mut smaller, mut greater) = (None, None);
for i in [a.trim(), b.trim()] {
if let Some(version) = i.strip_prefix(">=") {
smaller = Some(version.parse()?);
} else if let Some(version) = i.strip_prefix("<=") {
greater = Some(version.parse()?);
}
}
return Ok(Self::Between(
smaller.ok_or(VersionError::InvalidRange)?,
greater.ok_or(VersionError::InvalidRange)?,
));
}
if let Some(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::Between(a, b) => write!(f, ">={a}, <={b}"),
Self::Compatible(ver) => write!(f, "~{ver}"),
}
}
}
impl_serde_str!(VersionFilter);
/// An error caused by dealing with version or version filter strings.
#[derive(Debug, Clone, thiserror::Error)]
pub enum VersionError {
#[error("invalid namespace")]
ParseNamespace,
#[error("invalid main part")]
ParseMain,
#[error("invalid prerelease part")]
ParsePrerelease,
#[error("invalid version range")]
InvalidRange,
}
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(())
}
}