init: first cut.
library only. tests provide coverage of path for local file inclusion for a package build.
This commit is contained in:
parent
ad532a2818
commit
0037bca017
21 changed files with 3788 additions and 2 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
/.vscode
|
2609
Cargo.lock
generated
Normal file
2609
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
3
Cargo.toml
Normal file
3
Cargo.toml
Normal file
|
@ -0,0 +1,3 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["crates/*"]
|
24
crates/ia/Cargo.toml
Normal file
24
crates/ia/Cargo.toml
Normal file
|
@ -0,0 +1,24 @@
|
|||
[package]
|
||||
name = "ia"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
base16ct = { version = "0.2.0", features = ["alloc", "std"] }
|
||||
bytes = "1.7.2"
|
||||
cap-std = "3.2.0"
|
||||
cap-tempfile = "3.3.0"
|
||||
copy_on_write = "0.1.3"
|
||||
decompress = "0.6.0"
|
||||
digest = { version = "0.10.7", features = ["alloc", "std"] }
|
||||
hex = "0.4.3"
|
||||
reqwest = { version = "0.12.7", features = ["blocking"] }
|
||||
serde = { version = "1.0.210", features = ["derive"] }
|
||||
sha2 = "0.10.8"
|
||||
sha3 = "0.10.8"
|
||||
strum = { version = "0.26.3", features = ["derive"] }
|
||||
tar = "0.4.42"
|
||||
thiserror = "1.0.63"
|
||||
toml = "0.8.19"
|
||||
tracing = "0.1.40"
|
||||
url = "2.5.2"
|
81
crates/ia/src/composer/mod.rs
Normal file
81
crates/ia/src/composer/mod.rs
Normal file
|
@ -0,0 +1,81 @@
|
|||
use std::{io::Write, path::PathBuf};
|
||||
|
||||
use cap_std::ambient_authority;
|
||||
use cap_tempfile::TempDir;
|
||||
use tracing::info;
|
||||
|
||||
use crate::{error, phase, Dir, File, Manifest};
|
||||
|
||||
/// a build pipeline manager that handles creation of the build
|
||||
/// environment, execution of each phase in turn, and production of the final
|
||||
/// build output in the desired format.
|
||||
pub struct Composer {
|
||||
manifest: Manifest,
|
||||
composition_prefix: Dir,
|
||||
source_files: Vec<File>,
|
||||
}
|
||||
|
||||
impl Composer {
|
||||
/// creates a new composer from a parsed manifest.
|
||||
/// this function creates a temporary directory backing the composition.
|
||||
/// when the composer goes out of scope, the temporary direcytory and all
|
||||
/// of its contents are removed from disk.
|
||||
pub fn new(manifest: Manifest) -> Result<Self, error::Composer> {
|
||||
info!("creating new composer for package '{0}'…", manifest.name);
|
||||
let composition_prefix = TempDir::new(ambient_authority())
|
||||
.map(|d| {
|
||||
d.into_dir()
|
||||
.map_err(|_| error::Composer::IO(error::IO::TempDirCreate))
|
||||
})
|
||||
.map_err(|_| error::Composer::IO(error::IO::TempDirCreate))??;
|
||||
|
||||
let mut prefix_manifest_file = composition_prefix
|
||||
.create("manifest.toml")
|
||||
.map(File)
|
||||
.map_err(|_| error::Composer::IO(error::IO::TempCreate))?;
|
||||
|
||||
let manifest_content = toml::to_string(&manifest)
|
||||
.map_err(|_| error::Composer::Fetch(error::Fetch::ManifestParse))?;
|
||||
|
||||
info!("copying manifest file to composition directory…");
|
||||
prefix_manifest_file
|
||||
.write(manifest_content.as_bytes())
|
||||
.and_then(|_| prefix_manifest_file.flush())
|
||||
.map_err(|_| error::Composer::IO(error::IO::TempWrite))?;
|
||||
|
||||
Ok(Self {
|
||||
manifest,
|
||||
source_files: vec![],
|
||||
composition_prefix: Dir(composition_prefix),
|
||||
})
|
||||
}
|
||||
|
||||
/// executes the fetch phase of the composition.
|
||||
pub fn fetch_phase(self) -> Result<Self, error::Composer> {
|
||||
info!("executing fetch phase for '{0}'…", self.manifest.name);
|
||||
let fetch_phase = phase::fetch::Fetch::new(
|
||||
self.manifest.sources.iter().collect(),
|
||||
&self.composition_prefix,
|
||||
)
|
||||
.map_err(error::Composer::Fetch)?;
|
||||
|
||||
let source_files = fetch_phase.fetch().map_err(error::Composer::Fetch)?;
|
||||
|
||||
Ok(Self {
|
||||
manifest: self.manifest,
|
||||
source_files,
|
||||
composition_prefix: self.composition_prefix,
|
||||
})
|
||||
}
|
||||
|
||||
/// returns an array of all files fetched in the fetch phase.
|
||||
pub fn fetched_source_files(&self) -> &[File] {
|
||||
&self.source_files
|
||||
}
|
||||
|
||||
/// executes the full composition pipeline and returns the path to the
|
||||
/// final packaged output.
|
||||
pub fn compose(&self) -> Result<PathBuf, error::Composer> {
|
||||
todo!()
|
||||
}
|
||||
}
|
102
crates/ia/src/error.rs
Normal file
102
crates/ia/src/error.rs
Normal file
|
@ -0,0 +1,102 @@
|
|||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error<T: AsRef<dyn std::error::Error>> {
|
||||
#[error("manifest error: {0}")]
|
||||
Manifest(T),
|
||||
#[error("hash error: {0}")]
|
||||
Hash(T),
|
||||
#[error("source error: {0}")]
|
||||
Source(T),
|
||||
#[error("fetch phase error: {0}")]
|
||||
Fetch(T),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum File {
|
||||
#[error("unsupported file type")]
|
||||
UnsupportedType(String),
|
||||
#[error("io error: {0}")]
|
||||
IO(IO),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Fetch {
|
||||
#[error("unable to parse manifest file")]
|
||||
ManifestParse,
|
||||
#[error("no fetcher implemented for scheme '{0}'")]
|
||||
UnimplementedScheme(String),
|
||||
#[error("source url '{0}' invalid")]
|
||||
InvalidSourceUrl(String),
|
||||
#[error("io error: {0}")]
|
||||
IO(IO),
|
||||
#[error("no local working directory identified")]
|
||||
NoCwd,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Source {
|
||||
#[error("source url does not exist: {0}")]
|
||||
Nonexistent(String),
|
||||
#[error("source url does not possess a file base name: {0}")]
|
||||
NoBaseName(String),
|
||||
#[error("source url is an absolute path: {0}")]
|
||||
AbsolutePath(String), // due to sandboxing, this error should never occur.
|
||||
// any function returning this error must panic.
|
||||
#[error("permission denied attempting to access source url: {0}")]
|
||||
PermissionDenied(String),
|
||||
#[error("unable to parse source url: {0}")]
|
||||
Parse(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Manifest {
|
||||
#[error("unable to parse manifest")]
|
||||
Parse,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Hash {
|
||||
#[error("unable to parse hash: {0}")]
|
||||
Parse(String),
|
||||
#[error("invalid hash: {0}")]
|
||||
Invalid(String),
|
||||
#[error("invalid algorithm: {0}")]
|
||||
Algorithm(String),
|
||||
#[error("internal error")]
|
||||
Internal,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum IO {
|
||||
#[error("could not create file: {0}")]
|
||||
Create(String),
|
||||
#[error("could not read file")]
|
||||
Read,
|
||||
#[error("could not read file: {0}")]
|
||||
ReadNamed(String),
|
||||
#[error("could not write file: {0}")]
|
||||
WriteNamed(String),
|
||||
#[error("could not copy file")]
|
||||
Copy,
|
||||
#[error("could not create reflink: {0}:{1}")]
|
||||
Reflink(String, String),
|
||||
#[error("no unpacker found for archive: {0}")]
|
||||
NoUnpacker(String),
|
||||
#[error("could not create temporary file")]
|
||||
TempCreate,
|
||||
#[error("could not create temporary directory")]
|
||||
TempDirCreate,
|
||||
#[error("could not read temporary file")]
|
||||
TempRead,
|
||||
#[error("could not write to temporary file")]
|
||||
TempWrite,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Composer {
|
||||
#[error("fetch phase error: {0}")]
|
||||
Fetch(Fetch),
|
||||
#[error("io error: {0}")]
|
||||
IO(IO),
|
||||
}
|
140
crates/ia/src/hash.rs
Normal file
140
crates/ia/src/hash.rs
Normal file
|
@ -0,0 +1,140 @@
|
|||
//! an ia hash is made up of two parts: the hash algorithm and the hash itself.
|
||||
//! this is simply to allow forward-compatibility.
|
||||
//! hashes can be deserialised from strings representing the format `algorithm:value`.
|
||||
|
||||
use crate::error;
|
||||
use digest::{Digest, DynDigest};
|
||||
use serde::{
|
||||
de::Visitor,
|
||||
Deserialize, Deserializer, Serialize, Serializer,
|
||||
};
|
||||
use std::{
|
||||
fmt::Display,
|
||||
io::Write,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
/// a hash algorithm supported by ia.
|
||||
#[derive(
|
||||
strum::Display, strum::EnumString, Clone, Debug, Deserialize, Serialize, PartialEq, Eq,
|
||||
)]
|
||||
pub enum HashAlgorithm {
|
||||
#[strum(to_string = "sha2", ascii_case_insensitive)]
|
||||
Sha2,
|
||||
#[strum(to_string = "sha3", ascii_case_insensitive)]
|
||||
Sha3,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct HashValue(Vec<u8>);
|
||||
|
||||
impl HashValue {
|
||||
pub fn new(val: Box<[u8]>) -> Self {
|
||||
Self(val.as_ref().to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for HashValue {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", hex::encode(self.0.as_slice()))
|
||||
}
|
||||
}
|
||||
|
||||
/// an individual hash for a package, input, or output.
|
||||
/// hashes of package inputs are used to verify their authenticity.
|
||||
/// hashes of the package outputs allow the content-addressed storage model to work.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Hash {
|
||||
pub algorithm: HashAlgorithm,
|
||||
pub value: HashValue,
|
||||
}
|
||||
|
||||
impl Display for Hash {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}:{}", self.algorithm, self.value)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Hasher: DynDigest + Write {}
|
||||
|
||||
impl Hasher for sha2::Sha512 {}
|
||||
|
||||
impl Hasher for sha3::Sha3_512 {}
|
||||
|
||||
impl Hash {
|
||||
pub fn hasher_for(alg: &HashAlgorithm) -> Result<Box<dyn Hasher>, error::Hash> {
|
||||
match alg {
|
||||
HashAlgorithm::Sha2 => Ok(Box::new(sha2::Sha512::default())),
|
||||
HashAlgorithm::Sha3 => Ok(Box::new(sha3::Sha3_512::default())),
|
||||
}
|
||||
}
|
||||
|
||||
// creates a new hash with the provided algorithm and value.
|
||||
pub fn new(alg: HashAlgorithm, val: String) -> Result<Self, error::Hash> {
|
||||
Ok(Self {
|
||||
algorithm: alg,
|
||||
value: HashValue(val.as_bytes().to_owned()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Hash {
|
||||
type Err = error::Hash;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
s.to_lowercase()
|
||||
.split_once(':')
|
||||
.ok_or(error::Hash::Parse(s.to_owned()))
|
||||
.map(|(alg, val)| {
|
||||
HashAlgorithm::from_str(alg)
|
||||
.map_err(|_| error::Hash::Algorithm(alg.to_owned()))
|
||||
.map(|alg| (alg, val))
|
||||
})?
|
||||
.map(|(alg, val)| Hash::new(alg, val.to_owned()))?
|
||||
}
|
||||
}
|
||||
|
||||
struct HashVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for HashVisitor {
|
||||
type Value = Hash;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a hash string (`algorithm:value`)")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Hash::from_str(v).map_err(|_| E::invalid_type(serde::de::Unexpected::Str(v), &self))
|
||||
}
|
||||
|
||||
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
// all hashes are represented in lowercase.
|
||||
// this generally handles unicode, but
|
||||
// hashes currently used by ia use ascii only.
|
||||
self.visit_str(&v.to_lowercase())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Hash {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_string(HashVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Hash {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&format!("{}:{}", self.algorithm, self.value))
|
||||
}
|
||||
}
|
163
crates/ia/src/lib.rs
Normal file
163
crates/ia/src/lib.rs
Normal file
|
@ -0,0 +1,163 @@
|
|||
mod error;
|
||||
mod hash;
|
||||
use std::{
|
||||
io::{self, BufReader, BufWriter, Read, Seek, SeekFrom, Write},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use cap_std::{ambient_authority, fs::OpenOptions};
|
||||
use hash::HashValue;
|
||||
pub use hash::{Hash, HashAlgorithm};
|
||||
pub mod manifest;
|
||||
pub use manifest::Manifest;
|
||||
use tracing::{debug, warn};
|
||||
pub mod phase;
|
||||
|
||||
#[derive(strum::Display)]
|
||||
enum ArchiveType {
|
||||
#[strum(to_string = "bz2", ascii_case_insensitive)]
|
||||
Bz2,
|
||||
#[strum(to_string = "gz", ascii_case_insensitive)]
|
||||
Gz,
|
||||
#[strum(to_string = "xz", ascii_case_insensitive)]
|
||||
Xz,
|
||||
#[strum(to_string = "zstd", ascii_case_insensitive)]
|
||||
Zstd,
|
||||
#[strum(to_string = "tar", ascii_case_insensitive)]
|
||||
Tar,
|
||||
#[strum(to_string = "7z", ascii_case_insensitive)]
|
||||
SevenZip,
|
||||
#[strum(to_string = "zip", ascii_case_insensitive)]
|
||||
Zip,
|
||||
#[strum(to_string = "rar", ascii_case_insensitive)]
|
||||
Rar, // oh no
|
||||
}
|
||||
|
||||
#[derive(strum::Display)]
|
||||
enum FileType {
|
||||
Archive(ArchiveType),
|
||||
Other,
|
||||
}
|
||||
|
||||
pub struct Dir(pub cap_std::fs::Dir);
|
||||
|
||||
impl Dir {
|
||||
fn open_file(&self, file_name: String) -> Result<File, error::IO> {
|
||||
self.0
|
||||
.open_with(
|
||||
file_name.clone(),
|
||||
OpenOptions::new().create(true).write(true).read(true),
|
||||
)
|
||||
.map_err(|_| error::IO::Create(file_name))
|
||||
.map(File)
|
||||
}
|
||||
|
||||
/// creates a new file named `dst_basename` within `self.prefix`.
|
||||
/// this function uses capabilities. writing to files outside of
|
||||
/// `self.prefix` will fail.
|
||||
pub fn write_file(&self, src_file: &mut File, dst_basename: String) -> Result<File, error::IO> {
|
||||
// ensure we are at the start of the file stream.
|
||||
// in rare cases, the file is written after being otherwise
|
||||
// at least partially read.
|
||||
src_file.reset().map_err(|_| error::IO::Read)?;
|
||||
|
||||
let mut dst_file = self.open_file(dst_basename.clone())?;
|
||||
debug!("writing to file: {0}", dst_basename);
|
||||
|
||||
io::copy(src_file, &mut dst_file)
|
||||
.and_then(|_| dst_file.flush())
|
||||
.map_err(|_| error::IO::Copy)
|
||||
.and_then(|_| dst_file.reset())
|
||||
.map(|_| dst_file)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct File(cap_std::fs::File);
|
||||
|
||||
impl Read for File {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
self.0.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for File {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
self.0.write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
self.0.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl File {
|
||||
pub fn hash(&mut self, alg: HashAlgorithm) -> Result<Hash, error::Hash> {
|
||||
self.reset().map_err(|_| error::Hash::Internal)?;
|
||||
|
||||
let mut buf = String::new();
|
||||
self.read_to_string(&mut buf).unwrap();
|
||||
self.reset().unwrap();
|
||||
|
||||
let mut hasher = Hash::hasher_for(&alg)?;
|
||||
io::copy(self, &mut hasher).map_err(|_| error::Hash::Internal)?;
|
||||
let hash_value = hasher.finalize_reset();
|
||||
hasher.flush().map_err(|_| error::Hash::Internal)?;
|
||||
|
||||
let hash_value = HashValue::new(hash_value);
|
||||
|
||||
Ok(Hash {
|
||||
algorithm: alg,
|
||||
value: hash_value,
|
||||
})
|
||||
}
|
||||
|
||||
fn seek(&mut self, pos: SeekFrom) -> Result<u64, error::IO> {
|
||||
self.0.seek(pos).map_err(|_| error::IO::Read)
|
||||
}
|
||||
|
||||
fn reset(&mut self) -> Result<(), error::IO> {
|
||||
self.seek(SeekFrom::Start(0)).map(|_| ())
|
||||
}
|
||||
|
||||
fn read_ambient(path: PathBuf) -> Result<BufReader<File>, error::IO> {
|
||||
warn!("ia::File::read_ambient() called! this function is not sandboxed.");
|
||||
cap_std::fs::File::open_ambient_with(
|
||||
path.clone(),
|
||||
OpenOptions::new().read(true),
|
||||
ambient_authority(),
|
||||
)
|
||||
.map(File)
|
||||
.map(BufReader::new)
|
||||
.map_err(|_| error::IO::ReadNamed(path.to_string_lossy().to_string()))
|
||||
}
|
||||
|
||||
// this truly _should_ never happen, but may be useful in some rare cases.
|
||||
fn write_ambient(path: PathBuf) -> Result<BufWriter<File>, error::IO> {
|
||||
warn!("ia::File::write_ambient() called. this function is not sandboxed.");
|
||||
|
||||
cap_std::fs::File::open_ambient_with(
|
||||
path.clone(),
|
||||
OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true),
|
||||
ambient_authority(),
|
||||
)
|
||||
.map(|f| BufWriter::new(File(f)))
|
||||
.map_err(|_| error::IO::WriteNamed(path.to_string_lossy().to_string()))
|
||||
}
|
||||
|
||||
/// copies the contents of `self` to `dst`, consuming `self` and returning
|
||||
/// `dst`.
|
||||
fn copy_to(self, dst: File) -> Result<File, error::IO> {
|
||||
let mut read = BufReader::new(self);
|
||||
let mut write = BufWriter::new(dst);
|
||||
|
||||
io::copy(&mut read, &mut write)
|
||||
.and_then(|_| write.flush())
|
||||
.map_err(|_| error::IO::Copy)
|
||||
.and_then(|_| write.into_inner().map_err(|_| error::IO::Copy))
|
||||
}
|
||||
}
|
320
crates/ia/src/manifest/mod.rs
Normal file
320
crates/ia/src/manifest/mod.rs
Normal file
|
@ -0,0 +1,320 @@
|
|||
//! a package manifest is the 'recipe' for constructing an [ia::PackageLock].
|
||||
//! a full package manifest can be found in the `docs` directory.
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::de::Visitor;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
|
||||
use crate::{error, ArchiveType, FileType, Hash};
|
||||
|
||||
// an architecture supported by ia packages.
|
||||
// for the sake of easier future expansion,
|
||||
// the 'other' value allows for architectures
|
||||
// that may not be completely supported presently.
|
||||
#[derive(strum::Display, Debug, Deserialize, Serialize)]
|
||||
pub enum Architecture {
|
||||
#[strum(to_string = "all")]
|
||||
#[serde(rename = "all")]
|
||||
All,
|
||||
#[strum(to_string = "aarch64")]
|
||||
#[serde(rename = "aarch64")]
|
||||
Aarch64,
|
||||
#[strum(to_string = "amd64")]
|
||||
#[serde(rename = "amd64")]
|
||||
Amd64,
|
||||
#[strum(to_string = "riscv")]
|
||||
#[serde(rename = "riscv")]
|
||||
RiscV,
|
||||
#[strum(to_string = "other:{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
// a license supported by ia packages. these are represented
|
||||
// as a spdx license expression.
|
||||
#[derive(strum::Display, Debug, Deserialize, Serialize)]
|
||||
pub enum License {
|
||||
#[strum(to_string = "gpl-3.0-only")]
|
||||
#[serde(rename = "gpl-3.0-only")]
|
||||
Gpl3Only,
|
||||
#[strum(to_string = "gpl-3.0-or-later")]
|
||||
#[serde(rename = "gpl-3.0-or-later")]
|
||||
Gpl3OrLater,
|
||||
#[strum(to_string = "gpl-2.0-only")]
|
||||
#[serde(rename = "gpl-2.0-only")]
|
||||
Gpl2Only,
|
||||
#[strum(to_string = "gpl-2.0-or-later")]
|
||||
#[serde(rename = "gpl-2.0-or-later")]
|
||||
Gpl2OrLater,
|
||||
#[strum(to_string = "lgpl-3.0-only")]
|
||||
#[serde(rename = "lgpl-3.0-only")]
|
||||
Lgpl3Only,
|
||||
#[strum(to_string = "lgpl-3.0-or-later")]
|
||||
#[serde(rename = "lgpl-3.0-or-later")]
|
||||
Lgpl3OrLater,
|
||||
#[strum(to_string = "bsd-2-clause")]
|
||||
#[serde(rename = "bsd-2-clause")]
|
||||
Bsd2,
|
||||
#[strum(to_string = "bsd-3-clause")]
|
||||
#[serde(rename = "bsd-3-clause")]
|
||||
Bsd3,
|
||||
#[strum(to_string = "mit")]
|
||||
#[serde(rename = "mit")]
|
||||
Mit,
|
||||
#[strum(to_string = "mpl-2.0")]
|
||||
#[serde(rename = "mpl-2.0")]
|
||||
Mozilla2,
|
||||
#[strum(to_string = "other:{0}")]
|
||||
#[serde(rename = "other:{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
/// a package's name, usually defined by upstream.
|
||||
#[derive(Debug, Deserialize, PartialEq, Serialize)]
|
||||
pub struct Name(String);
|
||||
|
||||
impl Name {
|
||||
pub fn new<T: AsRef<str>>(n: T) -> Self {
|
||||
Self(n.as_ref().to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Name {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// a package's description, usually defined by upstream.
|
||||
#[derive(Debug, Deserialize, PartialEq, Serialize)]
|
||||
pub struct Description(String);
|
||||
|
||||
#[derive(strum::Display, Debug, Deserialize, PartialEq, Serialize)]
|
||||
pub enum VersionOper {
|
||||
#[strum(to_string = "<")]
|
||||
LessThan,
|
||||
#[strum(to_string = "<=")]
|
||||
LessThanOrEqual,
|
||||
#[strum(to_string = "=")]
|
||||
Equal,
|
||||
#[strum(to_string = ">=")]
|
||||
GreaterThanOrEqual,
|
||||
#[strum(to_string = ">")]
|
||||
GreaterThan,
|
||||
}
|
||||
|
||||
impl FromStr for VersionOper {
|
||||
type Err = error::Manifest;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"<=" => Ok(VersionOper::LessThanOrEqual),
|
||||
">=" => Ok(VersionOper::GreaterThanOrEqual),
|
||||
"=" => Ok(VersionOper::Equal),
|
||||
"<" => Ok(VersionOper::LessThan),
|
||||
">" => Ok(VersionOper::GreaterThan),
|
||||
_ => Err(error::Manifest::Parse),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a package revision.
|
||||
// this is used by maintainers where package updates are required
|
||||
// that do not include changes from upstream.
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, Eq)]
|
||||
pub struct Revision(u8);
|
||||
|
||||
impl Revision {
|
||||
pub fn new(rev: u8) -> Self {
|
||||
Self(rev)
|
||||
}
|
||||
}
|
||||
|
||||
/// a package's version.
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||
pub struct Version(String);
|
||||
|
||||
impl Display for Version {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Version {
|
||||
pub fn new<T: AsRef<str>>(upstream: T) -> Self {
|
||||
Version(upstream.as_ref().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// a dependency entry.
|
||||
// these may be represented in two variants:
|
||||
// a version-free name, which will pull the latest version
|
||||
// of the package as a dependency, or
|
||||
// a versioned name, which may use the following operators:
|
||||
// - `<=`, which indicates a package version of the provided value or below
|
||||
// - `>=`, which indicates a package version of the provided value or above
|
||||
// - `=`, which indicates a package version of the provided value exactly
|
||||
// - `<`, which indicates a package version below the provided value
|
||||
// - `>`, which indicates a package version above the provided value
|
||||
// version operators are searched in the following order:
|
||||
// `<=`, `>=`, `=`, `<`, `>`
|
||||
#[derive(Debug, PartialEq, Serialize)]
|
||||
pub struct Dependency(Name, Option<(VersionOper, Version)>);
|
||||
|
||||
impl Dependency {
|
||||
fn new(name: Name, opped_version: Option<(VersionOper, Version)>) -> Self {
|
||||
Self(name, opped_version)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Dependency {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match &self.1 {
|
||||
None => write!(f, "{}", self.0),
|
||||
Some((op, version)) => write!(f, "{}{}{}", self.0, op, version),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DependencyVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for DependencyVisitor {
|
||||
type Value = Dependency;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a version string (e.g., `package_name=1.0`)")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
["<=", ">=", "=", "<", ">"]
|
||||
.into_iter()
|
||||
.find_map(|oper| {
|
||||
s.rsplit_once(oper)
|
||||
.map(|(name, version)| {
|
||||
(
|
||||
Name::new(name),
|
||||
VersionOper::from_str(oper).unwrap_or(VersionOper::Equal),
|
||||
Version::new(version),
|
||||
)
|
||||
})
|
||||
.map(|(n, o, v)| Dependency::new(n, Some((o, v))))
|
||||
})
|
||||
.or(Some(Dependency::new(Name::new(s), None)))
|
||||
.ok_or(E::invalid_value(serde::de::Unexpected::Str(s), &self))
|
||||
}
|
||||
|
||||
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
// all hashes are represented in lowercase.
|
||||
// this generally handles unicode, but
|
||||
// hashes currently used by ia use ascii only.
|
||||
self.visit_str(&v.to_lowercase())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Dependency {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_string(DependencyVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
/// a configuration phase construct which either uses a helper,
|
||||
/// or executes a collection of commands.
|
||||
#[derive(Debug, Deserialize, PartialEq, Serialize)]
|
||||
pub struct ConfigurePhase {
|
||||
handler: Option<String>,
|
||||
exec: Option<String>,
|
||||
}
|
||||
|
||||
/// a build phase construct which either uses a helper,
|
||||
/// or executes a collection of commands.
|
||||
#[derive(Debug, Deserialize, PartialEq, Serialize)]
|
||||
pub struct BuildPhase {
|
||||
handler: Option<String>,
|
||||
exec: Option<String>,
|
||||
}
|
||||
|
||||
/// a test phase construct which either uses a helper,
|
||||
/// or executes a collection of commands.
|
||||
#[derive(Debug, Deserialize, PartialEq, Serialize)]
|
||||
pub struct TestPhase {
|
||||
handler: Option<String>,
|
||||
exec: Option<String>,
|
||||
}
|
||||
|
||||
/// a source entry, or "input". a url pointing to a source file of some
|
||||
/// description.
|
||||
/// the currently-supported scheme handlers are:
|
||||
/// - `file:`, which is a local file
|
||||
/// - `http://` and `https://`, which interact over HTTP/HTTPS protocols
|
||||
/// respectively
|
||||
/// - `git://`, which interacts over the git DVCS protocol
|
||||
/// - `sftp://`, which interacts over the SSH File Transfer Protocol
|
||||
#[derive(Debug, Deserialize, PartialEq, Serialize)]
|
||||
pub struct Source {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
pub hash: Hash,
|
||||
}
|
||||
|
||||
impl Source {
|
||||
pub fn new(name: String, url: String, hash: Hash) -> Self {
|
||||
Self { name, url, hash }
|
||||
}
|
||||
}
|
||||
|
||||
impl Source {
|
||||
/// attempts to match the filetype using the extension of the source
|
||||
/// destination's basename.
|
||||
///
|
||||
/// this isn't a particularly resilient heuristic against naming files
|
||||
/// with invalid or no extension, but it's reasonable enough to fix in the
|
||||
/// manifest and increases clarity of the lockfile.
|
||||
fn dst_file_type(&self) -> Result<FileType, error::File> {
|
||||
match self
|
||||
.name
|
||||
.clone()
|
||||
.rsplit_once('.')
|
||||
.ok_or(error::File::UnsupportedType("none".to_owned()))?
|
||||
.1
|
||||
{
|
||||
"gz" => Ok(FileType::Archive(ArchiveType::Gz)),
|
||||
"xz" => Ok(FileType::Archive(ArchiveType::Xz)),
|
||||
"zstd" => Ok(FileType::Archive(ArchiveType::Zstd)),
|
||||
"bz2" => Ok(FileType::Archive(ArchiveType::Bz2)),
|
||||
"rar" => Ok(FileType::Archive(ArchiveType::Rar)),
|
||||
"zip" => Ok(FileType::Archive(ArchiveType::Zip)),
|
||||
"7z" => Ok(FileType::Archive(ArchiveType::SevenZip)),
|
||||
_ => Ok(FileType::Other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// a full package manifest.
|
||||
/// manifests are a representation of the "recipe" used to build the package.
|
||||
/// a manifest is used to build an ia package and generate its lockfile thereafter.
|
||||
#[derive(Debug, Deserialize, PartialEq, Serialize)]
|
||||
pub struct Manifest {
|
||||
pub name: Name,
|
||||
pub description: Description,
|
||||
pub version: Version,
|
||||
pub revision: Revision,
|
||||
pub sources: Vec<Source>,
|
||||
pub patches: Option<Vec<Source>>,
|
||||
pub dependencies: Vec<Dependency>,
|
||||
pub build_dependencies: Vec<Dependency>,
|
||||
#[serde(rename = "configure")]
|
||||
pub configure_phase: ConfigurePhase,
|
||||
#[serde(rename = "build")]
|
||||
pub build_phase: BuildPhase,
|
||||
#[serde(rename = "test")]
|
||||
pub test_phase: TestPhase,
|
||||
}
|
62
crates/ia/src/phase/fetch/file.rs
Normal file
62
crates/ia/src/phase/fetch/file.rs
Normal file
|
@ -0,0 +1,62 @@
|
|||
//! the local file implementation of [super::Fetcher], handling the fetching of
|
||||
//! source files from a local filesystem.
|
||||
|
||||
|
||||
use crate::{error, manifest::Source, Dir, File};
|
||||
use std::io::{self, BufWriter, Write};
|
||||
pub(crate) struct Fetcher {}
|
||||
|
||||
/// the local file implementation of [super::Fetcher].
|
||||
/// usage of this fetcher in application code is discouraged, as source file
|
||||
/// retrieval cannot be sandboxed and therefore can be used to access files
|
||||
/// outside of the build environment.
|
||||
impl super::Fetcher for Fetcher {
|
||||
fn new() -> Result<Self, error::Fetch> {
|
||||
Ok(Self {})
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"file"
|
||||
}
|
||||
|
||||
/// opens an arbitrary file with read-only permissions, creates a file
|
||||
/// within the provided `dst_dir`, and copies the contents of the source
|
||||
/// file to the destination file.
|
||||
///
|
||||
/// this function mutates data, and must never be used implicitly in
|
||||
/// application code.
|
||||
fn fetch(&self, src: &Source, dst_dir: &Dir) -> Result<File, error::Fetch> {
|
||||
let src_path_string = src
|
||||
.url
|
||||
.split_once("file:")
|
||||
.map(|(_, p)| p.to_string())
|
||||
.ok_or(error::Fetch::InvalidSourceUrl(src.url.clone()))?;
|
||||
|
||||
let src_path = std::env::current_dir()
|
||||
.map_err(|_| error::Fetch::NoCwd)?
|
||||
.join(src_path_string.clone());
|
||||
|
||||
let mut src_buf = File::read_ambient(src_path)
|
||||
.map_err(|_| error::Fetch::IO(error::IO::ReadNamed(src_path_string.clone())))?;
|
||||
|
||||
let dst_file = dst_dir
|
||||
.open_file(src.name.clone())
|
||||
.map_err(error::Fetch::IO)?;
|
||||
|
||||
let mut dst_buf = BufWriter::new(dst_file);
|
||||
io::copy(&mut src_buf, &mut dst_buf)
|
||||
// reports of flushing on copy are
|
||||
// greatly exaggerated.
|
||||
// enforce this to ensure all data
|
||||
// is written to disk or explicitly
|
||||
// fails.
|
||||
.and_then(|_| dst_buf.flush())
|
||||
.map(|_| {
|
||||
dst_buf
|
||||
.into_inner()
|
||||
.map_err(|_| error::Fetch::IO(error::IO::WriteNamed(src_path_string.clone())))
|
||||
})
|
||||
.map_err(|_| error::Fetch::IO(error::IO::WriteNamed(src_path_string.clone())))?
|
||||
.map(|mut f| f.reset().map_err(error::Fetch::IO).map(|_| f))?
|
||||
}
|
||||
}
|
77
crates/ia/src/phase/fetch/mod.rs
Normal file
77
crates/ia/src/phase/fetch/mod.rs
Normal file
|
@ -0,0 +1,77 @@
|
|||
//! the fetch phase is the first stage of the package composition pipeline.
|
||||
//! it is responsible for fetching the sources of the package from its
|
||||
//! defined source paths using an automatically determined scheme handler.
|
||||
//!
|
||||
//! this phase is transactional:
|
||||
//! - sources are not linked into the build directory until all sources
|
||||
//! are successfully obtained.
|
||||
//! - if the parsing or retrieval of any input fails,
|
||||
//! the entire phase will return an error and the retrieved sources
|
||||
//! are removed from disk.
|
||||
//!
|
||||
//! fetch handlers generally stream source artifacts in chunks instead
|
||||
//! of reading the entire artifact into memory, allowing sources of sizes
|
||||
//! much larger than the available system memory to be used.
|
||||
mod file;
|
||||
use crate::{error, manifest::Source, Dir, File, HashAlgorithm};
|
||||
use tracing::info;
|
||||
|
||||
pub trait Fetcher {
|
||||
fn new() -> Result<Self, error::Fetch>
|
||||
where
|
||||
Self: Sized;
|
||||
fn fetch(&self, src: &Source, dst_dir: &Dir) -> Result<File, error::Fetch>;
|
||||
fn name(&self) -> &'static str;
|
||||
}
|
||||
|
||||
pub struct Fetch<'a> {
|
||||
sources: Vec<&'a Source>,
|
||||
prefix: &'a Dir,
|
||||
}
|
||||
|
||||
impl<'a> Fetch<'a> {
|
||||
pub fn new(sources: Vec<&'a Source>, prefix: &'a Dir) -> Result<Self, error::Fetch> {
|
||||
Ok(Self { sources, prefix })
|
||||
}
|
||||
|
||||
pub fn fetcher_for(source: &Source) -> Result<Box<dyn Fetcher>, error::Fetch> {
|
||||
match source
|
||||
.url
|
||||
.split_once(':')
|
||||
.map(|split| split.0)
|
||||
.or(Some(""))
|
||||
.ok_or(error::Fetch::InvalidSourceUrl(source.url.clone()))?
|
||||
{
|
||||
"file" => Ok(Box::new(file::Fetcher::new()?)),
|
||||
"http" | "https" => todo!(),
|
||||
"ftp" => todo!(),
|
||||
"git" => todo!(),
|
||||
"sftp" => todo!(),
|
||||
_ => Err(error::Fetch::UnimplementedScheme(source.url.clone())),
|
||||
}
|
||||
}
|
||||
|
||||
fn _fetch(&self, source: &Source) -> Result<File, error::Fetch> {
|
||||
let fetcher: Box<dyn Fetcher> = Fetch::fetcher_for(source)?;
|
||||
info!(
|
||||
"executing '{0}' fetcher for source '{1}'…",
|
||||
fetcher.name(),
|
||||
source.name
|
||||
);
|
||||
|
||||
let mut file = fetcher.fetch(source, self.prefix).unwrap();
|
||||
println!("{:?}", file.hash(HashAlgorithm::Sha2).unwrap());
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
pub fn fetch(&self) -> Result<Vec<File>, error::Fetch> {
|
||||
// FIX: this is presently quite inefficient, as it recreates a new
|
||||
// fetcher for each source even if they are all of the same scheme.
|
||||
// could cache a fetcher upon its creation and re-use it, but would
|
||||
// need a 'fetcher pool' for this.
|
||||
self.sources
|
||||
.iter()
|
||||
.map(|source| self._fetch(source))
|
||||
.collect::<Result<Vec<File>, error::Fetch>>()
|
||||
}
|
||||
}
|
1
crates/ia/src/phase/mod.rs
Normal file
1
crates/ia/src/phase/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod fetch;
|
10
crates/tests/Cargo.toml
Normal file
10
crates/tests/Cargo.toml
Normal file
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "ia-tests"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
cap-tempfile = "3.3.0"
|
||||
ia = { path = "../ia" }
|
||||
toml = "0.8.19"
|
||||
url = { version = "2.5.2", features = ["serde"] }
|
28
crates/tests/fixtures/package/manifest.toml
vendored
Normal file
28
crates/tests/fixtures/package/manifest.toml
vendored
Normal file
|
@ -0,0 +1,28 @@
|
|||
name = "test-package"
|
||||
description = "a test package for apm testing"
|
||||
version = "1.2.3-git"
|
||||
revision = 42
|
||||
license = "gpl-3.0-only"
|
||||
architectures = [ "ppc64" ]
|
||||
dependencies = [ ]
|
||||
build_dependencies = [ ]
|
||||
provides = [ "hello-world" ]
|
||||
groups = [ "examples" ]
|
||||
|
||||
[[sources]]
|
||||
name = "source-test.txt"
|
||||
url = "file:./source-test.txt"
|
||||
hash = "sha2:4cbf04bd094e2bd2eda36cf3561bfa61cb1a83c29427f0dd01d8b071cd8d002f3f46aa1c8b5eef25e7743f629ad4976f59bf7ee07d345b9bdb89c85703e581c7"
|
||||
|
||||
[configure]
|
||||
exec = """
|
||||
"""
|
||||
|
||||
[build]
|
||||
exec = """
|
||||
make
|
||||
"""
|
||||
|
||||
[test]
|
||||
exec = """
|
||||
"""
|
1
crates/tests/fixtures/package/source-test.txt
vendored
Normal file
1
crates/tests/fixtures/package/source-test.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
test source file
|
51
crates/tests/src/hash.rs
Normal file
51
crates/tests/src/hash.rs
Normal file
|
@ -0,0 +1,51 @@
|
|||
|
||||
use std::str::FromStr;
|
||||
|
||||
use ia::{Hash, HashAlgorithm};
|
||||
|
||||
const HASH_PLAINTEXT: &str = "example";
|
||||
|
||||
pub(crate) const SHA2_HASH_STR: &str = "sha2:3bb12eda3c298db5de25597f54d924f2e17e78a26ad8953ed8218ee682f0bbbe9021e2f3009d152c911bf1f25ec683a902714166767afbd8e5bd0fb0124ecb8a";
|
||||
pub(crate) const SHA2_HASH_ALG: HashAlgorithm = HashAlgorithm::Sha2;
|
||||
pub(crate) const SHA2_HASH_VALUE: &str = "3bb12eda3c298db5de25597f54d924f2e17e78a26ad8953ed8218ee682f0bbbe9021e2f3009d152c911bf1f25ec683a902714166767afbd8e5bd0fb0124ecb8a";
|
||||
pub(crate) const SHA3_HASH_STR: &str = "sha3:e6da59e7349fb06a3de52ab3a2b383090f80d45ea8489d76b231d580ccf01fb112e509dedd5cc09bead4aa54455ca4c66cd46abe35c061325802d5df12b2a55d";
|
||||
pub(crate) const SHA3_HASH_ALG: HashAlgorithm = HashAlgorithm::Sha3;
|
||||
pub(crate) const SHA3_HASH_VALUE: &str = "e6da59e7349fb06a3de52ab3a2b383090f80d45ea8489d76b231d580ccf01fb112e509dedd5cc09bead4aa54455ca4c66cd46abe35c061325802d5df12b2a55d";
|
||||
pub(crate) const INVALIDALG_HASH_STR: &str = "abcd213:3bb12eda3c298db5de25597f54d924f2e17e78a26ad8953ed8218ee682f0bbbe9021e2f3009d152c911bf1f25ec683a902714166767afbd8e5bd0fb0124ecb8a";
|
||||
pub(crate) const INVALIDVALUE_HASH_STR: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
||||
|
||||
#[test]
|
||||
fn can_parse_sha2_alg() {
|
||||
let hash = Hash::from_str(SHA2_HASH_STR).unwrap();
|
||||
assert_eq!(hash.algorithm, SHA2_HASH_ALG)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_parse_sha2_value() {
|
||||
let hash = Hash::from_str(SHA2_HASH_STR).unwrap();
|
||||
assert_eq!(hash.value.to_string(), SHA2_HASH_VALUE.to_owned())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_parse_sha3_alg() {
|
||||
let hash = Hash::from_str(SHA3_HASH_STR).unwrap();
|
||||
assert_eq!(hash.algorithm, SHA3_HASH_ALG)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_parse_sha3_value() {
|
||||
let hash = Hash::from_str(SHA3_HASH_STR).unwrap();
|
||||
assert_eq!(hash.value.to_string(), SHA3_HASH_VALUE.to_owned())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_alg() {
|
||||
let hash = Hash::from_str(INVALIDALG_HASH_STR);
|
||||
assert!(hash.is_err())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_hash() {
|
||||
let hash = Hash::from_str(INVALIDVALUE_HASH_STR);
|
||||
assert!(hash.is_err())
|
||||
}
|
3
crates/tests/src/lib.rs
Normal file
3
crates/tests/src/lib.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
mod hash;
|
||||
mod manifest;
|
||||
mod phase;
|
31
crates/tests/src/manifest/mod.rs
Normal file
31
crates/tests/src/manifest/mod.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
use ia::{manifest::{Revision, Version}, Manifest};
|
||||
|
||||
pub(crate) const MANIFEST_FILE_STR: &str = include_str!("../../fixtures/package/manifest.toml");
|
||||
pub(crate) const MANIFEST_PACKAGE_NAME_STR: &str = "test-package";
|
||||
pub(crate) const MANIFEST_VERSION_STR: &str = "1.2.3-git";
|
||||
pub(crate) const MANIFEST_REVISION: u8 = 42;
|
||||
|
||||
#[test]
|
||||
fn can_parse_toml() {
|
||||
let manifest_toml: toml::Table = toml::from_str(MANIFEST_FILE_STR).unwrap();
|
||||
assert_eq!(
|
||||
manifest_toml["name"].as_str(),
|
||||
Some(MANIFEST_PACKAGE_NAME_STR)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_parse_manifest() {
|
||||
let manifest: Manifest = toml::from_str(MANIFEST_FILE_STR).unwrap();
|
||||
assert_eq!(manifest.name.to_string(), MANIFEST_PACKAGE_NAME_STR);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_parse_version() {
|
||||
let version = Version::new(MANIFEST_VERSION_STR);
|
||||
let revision = Revision::new(MANIFEST_REVISION);
|
||||
let manifest: Manifest = toml::from_str(MANIFEST_FILE_STR).unwrap();
|
||||
|
||||
assert_eq!(manifest.version, version);
|
||||
assert_eq!(manifest.revision, revision);
|
||||
}
|
71
crates/tests/src/phase/fetch/mod.rs
Normal file
71
crates/tests/src/phase/fetch/mod.rs
Normal file
|
@ -0,0 +1,71 @@
|
|||
|
||||
use std::io::Read;
|
||||
|
||||
use cap_tempfile::ambient_authority;
|
||||
use cap_tempfile::TempDir;
|
||||
use ia::manifest::Source;
|
||||
use ia::phase::fetch::Fetch;
|
||||
use ia::Dir;
|
||||
use ia::File;
|
||||
use ia::Hash;
|
||||
use ia::Manifest;
|
||||
|
||||
const SOURCE_FILE_NAME: &str = "source-test.txt";
|
||||
const SOURCE_FILE_CONTENT: &str = "test source file\n";
|
||||
const SOURCE_FILE_HASH: &str = "0c984b1b4ebc120d693d33ebda78d26fd91f27614de387c420960cc67a0e32ffef2a7adfaf132acf411c2606a10e0290822b5ac096471342c05b89d62230e486";
|
||||
|
||||
fn test_source() -> Source {
|
||||
Source::new(
|
||||
SOURCE_FILE_NAME.to_string(),
|
||||
String::from("file:./fixtures/package/source-test.txt"),
|
||||
Hash::new(ia::HashAlgorithm::Sha2, String::new()).unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
fn test_source_file() -> File {
|
||||
Fetch::fetcher_for(&test_source())
|
||||
.unwrap()
|
||||
.fetch(&test_source(), &test_build_prefix())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn test_manifest() -> Manifest {
|
||||
toml::from_str(crate::manifest::MANIFEST_FILE_STR).unwrap()
|
||||
}
|
||||
|
||||
fn test_build_prefix() -> Dir {
|
||||
Dir(TempDir::new(ambient_authority())
|
||||
.unwrap()
|
||||
.into_dir()
|
||||
.unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_identify_fetcher_from_url() {
|
||||
assert_eq!(
|
||||
Fetch::fetcher_for(test_manifest().sources.first().unwrap())
|
||||
.unwrap()
|
||||
.name(),
|
||||
"file"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_fetch() {
|
||||
let mut buf = String::new();
|
||||
test_source_file().read_to_string(&mut buf).unwrap();
|
||||
|
||||
assert_eq!(buf, SOURCE_FILE_CONTENT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_hash_source() {
|
||||
assert_eq!(
|
||||
test_source_file()
|
||||
.hash(ia::HashAlgorithm::Sha2)
|
||||
.unwrap()
|
||||
.value
|
||||
.to_string(),
|
||||
SOURCE_FILE_HASH
|
||||
);
|
||||
}
|
1
crates/tests/src/phase/mod.rs
Normal file
1
crates/tests/src/phase/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
mod fetch;
|
10
readme.md
10
readme.md
|
@ -85,11 +85,14 @@ if you really need a longer name to attach to it, i guess 'installation assistan
|
|||
- ia repository server
|
||||
- package definition import from:
|
||||
- apk: apkbuild
|
||||
- implementation of specific configuration / pre-build handlers
|
||||
- autoconf
|
||||
- implementation of specific build handlers
|
||||
- cmake
|
||||
- go
|
||||
- cargo
|
||||
- meson
|
||||
- cpan
|
||||
- python (setup.py)
|
||||
- implementation of specific test handlers
|
||||
- cmake
|
||||
- go
|
||||
|
@ -103,6 +106,9 @@ if you really need a longer name to attach to it, i guess 'installation assistan
|
|||
- implementation of further download handlers
|
||||
- hg
|
||||
- bzr
|
||||
- implementation of further build handlers
|
||||
- cargo
|
||||
- python (distutils)
|
||||
- package definition import from:
|
||||
- apk: apkbuild
|
||||
- pacman: pkgbuild
|
||||
|
@ -117,7 +123,7 @@ if you really need a longer name to attach to it, i guess 'installation assistan
|
|||
* deb: debian control
|
||||
* library package definition import from:
|
||||
* javascript: npm
|
||||
* ruby: rubygems
|
||||
* ruby: rubygems (via git)
|
||||
* python: pypi
|
||||
* rust: crates.io
|
||||
* go: pkg.dev
|
||||
|
|
Loading…
Reference in a new issue