Checkpoint
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "nye-package"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { version = "1.0.104", features = ["backtrace"] }
|
||||
semver = { version = "1.0.28", features = ["serde"] }
|
||||
serde = { version = "1.0.229", features = ["derive"] }
|
||||
tempfile = "3.27.0"
|
||||
tokio = { version = "1.53.1", default-features = false, features = ["fs"] }
|
||||
toml = { version = "1.1.4", features = ["preserve_order"] }
|
||||
@@ -0,0 +1,47 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use semver::Version;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::shared::Architecture;
|
||||
|
||||
/// An installable package's manifest, compiled from a [`Manifest`](crate::manifest::Manifest).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InstallableManifest {
|
||||
/// The package's name, version, and architecture.
|
||||
pub package: InstallableManifestPackage,
|
||||
|
||||
/// The exposed artifacts of the package, such as binaries or libraries.
|
||||
pub exposed: InstallableManifestExposed,
|
||||
}
|
||||
|
||||
/// The installable package's name, version, and architecture.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InstallableManifestPackage {
|
||||
/// The package's name.
|
||||
pub name: String,
|
||||
|
||||
/// The package's version.
|
||||
pub version: Version,
|
||||
|
||||
/// The installable package's architecture.
|
||||
pub architecture: Architecture,
|
||||
}
|
||||
|
||||
/// The installable package's exposed artifacts, such as binaries or libraries.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InstallableManifestExposed {
|
||||
/// The installable package's exposed binaries.
|
||||
#[serde(rename = "bin")]
|
||||
pub binaries: Vec<InstallableManifestExposedBinary>,
|
||||
}
|
||||
|
||||
/// An installable package's exposed binary.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InstallableManifestExposedBinary {
|
||||
/// The name as it'll be exposed in the system.
|
||||
pub name: String,
|
||||
|
||||
/// The path to the binary, relative to the `Installable.toml` file.
|
||||
pub path: PathBuf,
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
//! Manifest and utilities to handle package installables.
|
||||
|
||||
pub mod manifest;
|
||||
@@ -0,0 +1,6 @@
|
||||
//! This crate provides functionality for managing Nye packages, including bundling, installing,
|
||||
//! and uninstalling them.
|
||||
|
||||
pub mod installable;
|
||||
pub mod project;
|
||||
pub mod shared;
|
||||
@@ -0,0 +1,135 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::Context;
|
||||
use semver::Version;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs;
|
||||
|
||||
use crate::shared::Architecture;
|
||||
|
||||
/// A Nye package manifest.
|
||||
///
|
||||
/// It describes the package's general information, dependencies, exposed artifacts, and other
|
||||
/// things. It's used by the Nye package manager to know how to install, update, and remove the
|
||||
/// package.
|
||||
///
|
||||
/// This is currently stored at the package's root directory in a file called `Nye.toml`. The file
|
||||
/// is in TOML format, and is deserialized into this struct when the package manager needs to read
|
||||
/// it.
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct ProjectManifest {
|
||||
/// The package's general information, such as its name, version, and description.
|
||||
pub package: ProjectManifestPackage,
|
||||
|
||||
/// The package's exposed binaries, libraries, and other artifacts.
|
||||
pub exposed: ProjectManifestExposed,
|
||||
}
|
||||
|
||||
impl ProjectManifest {
|
||||
/// Validates the manifest, ensuring that the values are well-formed and consistent.
|
||||
///
|
||||
/// Returns:
|
||||
/// * `Ok(())` - When the manifest is valid.
|
||||
/// * `Err(anyhow::Error)` - When the manifest is invalid, with a message describing the issue.
|
||||
pub fn validate(&self) -> anyhow::Result<()> {
|
||||
if self.package.architectures.is_empty() {
|
||||
anyhow::bail!("The package must support at least one architecture.");
|
||||
}
|
||||
|
||||
for (i, architecture) in self.package.architectures.iter().enumerate() {
|
||||
if self
|
||||
.package
|
||||
.architectures
|
||||
.iter()
|
||||
.position(|a| a == architecture)
|
||||
!= Some(i)
|
||||
{
|
||||
anyhow::bail!(
|
||||
"The package's architectures must be unique, yet {:?} appears more than once.",
|
||||
architecture
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A Nye package's general information, such as its name, version, and description.
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct ProjectManifestPackage {
|
||||
/// The package's name, which is used to identify it in the Nye package manager.
|
||||
pub name: String,
|
||||
|
||||
/// The package's version, which is used to identify it in the Nye package manager.
|
||||
pub version: Version,
|
||||
|
||||
/// The package's description, which is used to describe it in the Nye package manager.
|
||||
pub description: Option<String>,
|
||||
|
||||
/// The package's supported architectures, which is used to determine if the package can be
|
||||
/// installed on the current system.
|
||||
pub architectures: Vec<Architecture>,
|
||||
}
|
||||
|
||||
/// A Nye package's exposed binaries, libraries, and other artifacts.
|
||||
///
|
||||
/// Only the artifacts listed here are exposed to the user and dependants.
|
||||
#[derive(Default, Clone, Serialize, Deserialize)]
|
||||
pub struct ProjectManifestExposed {
|
||||
/// The package's exposed binaries, which are the executables that the package provides to the
|
||||
/// user.
|
||||
#[serde(rename = "bin")]
|
||||
pub binaries: Vec<ProjectManifestExposeBinary>,
|
||||
}
|
||||
|
||||
/// A Nye package's exposed binary, which is an executable that the package provides to the user.
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct ProjectManifestExposeBinary {
|
||||
/// The name under which the binary is exposed.
|
||||
pub name: String,
|
||||
|
||||
/// The path to the binary, relative to the `./src/{arch}/bin` directory (which is itself
|
||||
/// relative to the `Nye.toml` file).
|
||||
///
|
||||
/// If missing, it is assumed that the binary is located at `./src/{arch}/bin/{name}`.
|
||||
pub path: Option<PathBuf>,
|
||||
|
||||
/// The architectures for which the binary is available.
|
||||
///
|
||||
/// If not specified, it is assumed that the binary is available for all architectures
|
||||
/// specified in the `package.architectures` field.
|
||||
pub architectures: Option<Vec<Architecture>>,
|
||||
}
|
||||
|
||||
/// Reads a Nye package's manifest from the given path and returns it as a [`Manifest`] struct.
|
||||
///
|
||||
/// Arguments:
|
||||
/// * `path` - The path to the manifest file. This is usually the `Nye.toml` file at the root of
|
||||
/// the package's directory.
|
||||
///
|
||||
/// Returns:
|
||||
/// * `Ok(Manifest)` - The manifest struct if the file was read and parsed successfully.
|
||||
/// * `Err(anyhow::Error)` - An error if the file could not be read or parsed.
|
||||
pub async fn read(path: impl AsRef<Path>) -> anyhow::Result<ProjectManifest> {
|
||||
let string = fs::read_to_string(path)
|
||||
.await
|
||||
.context("Could not read package's manifest.")?;
|
||||
|
||||
parse(&string)
|
||||
}
|
||||
|
||||
/// Parses a Nye package's manifest from the given string and returns it as a [`Manifest`] struct.
|
||||
///
|
||||
/// Arguments:
|
||||
/// * `string` - The string containing the manifest in TOML format.
|
||||
///
|
||||
/// Returns:
|
||||
/// * `Ok(Manifest)` - The manifest struct if the string was parsed successfully.
|
||||
/// * `Err(anyhow::Error)` - An error if the string could not be parsed.
|
||||
pub fn parse(string: &str) -> anyhow::Result<ProjectManifest> {
|
||||
let manifest: ProjectManifest =
|
||||
toml::from_str(string).context("Could not parse the package's manifest.")?;
|
||||
|
||||
Ok(manifest)
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
//! Manifest and utilities to handle package projects.
|
||||
//!
|
||||
//! ## Package Project Structure
|
||||
//!
|
||||
//! A Nye package project is a project that contains the files to bundle into a Nye package,
|
||||
//! together with a `Nye.toml` manifest file that describes the package's metadata and
|
||||
//! configuration.
|
||||
//!
|
||||
//! A package project's structure looks like follows:
|
||||
//!
|
||||
//! ```text
|
||||
//! package/ # The root directory of the package project
|
||||
//! Nye.toml # The package's manifest file
|
||||
//! shared/ # A directory containing files that are shared across all architectures of the package.
|
||||
//! bin/ # A directory containing the package's binaries that are shared across all architectures.
|
||||
//! {architecture}/ # A directory for each architecture that the package supports, containing the files to bundle for that architecture.
|
||||
//! bin/ # A directory containing the package's binaries for that architecture.
|
||||
//! ```
|
||||
//!
|
||||
//! The architectures supported are those returnable by [`std::env::consts::ARCH`], such as
|
||||
//! `x86_64`, `aarch64`, and `arm`. The architecture-specific directories must match the names of
|
||||
//! the architectures specified in the `package.architectures` field of the `Nye.toml` manifest
|
||||
//! file.
|
||||
//!
|
||||
//! When an artifact is not available for a specific architecture, it'll fall back to the shared
|
||||
//! version of the artifact if it exists.
|
||||
//!
|
||||
//! ### Package Project's Manifest File
|
||||
//!
|
||||
//! The `Nye.toml` manifest file is a TOML file at the package project's root directory that
|
||||
//! describes the package's metadata and configuration.
|
||||
//!
|
||||
//! ```toml
|
||||
//! [package]
|
||||
//! name = "my-package" # The package's name
|
||||
//! version = "1.0.0" # The package's version
|
||||
//! architectures = ["x86_64", "aarch64"] # The architectures that the package supports
|
||||
//!
|
||||
//! [[exposed.bin]]
|
||||
//! name = "my-binary" # The name of the binary as it'll be exposed in the system
|
||||
//! path = "my-binary" # The path to the binary inside each `bin/` directory
|
||||
//! architectures = ["x86_64", "aarch64"] # The architectures that this binary is available for
|
||||
//! ```
|
||||
//!
|
||||
//! The supported fields are the following:
|
||||
//!
|
||||
//! - `package.name` is the name of the package. It'll be used by (future) repositories to identify
|
||||
//! the package.
|
||||
//! - `package.version` is the semver version of the package. It'll be used by (future)
|
||||
//! repositories and dependencies to identify the package's version.
|
||||
//! - `package.architectures` is an array of the architectures that the package supports.
|
||||
//!
|
||||
//! - `exposed.bin` is an array of the binaries that the package exposes. Not all binaries
|
||||
//! available in a package are exposed to the user and dependants. Only the binaries listed in
|
||||
//! this array will be added to the `PATH` and made available to the user.
|
||||
//! - `exposed.bin.name` is the name of the binary as it'll be exposed in the system. This is the
|
||||
//! name that the user and dependants will use to invoke the binary.
|
||||
//! - `exposed.bin.path` is the path to the binary inside each `bin/` directory, or under
|
||||
//! `shared/bin/` if the binary is not available for an architecture. When not specified, it'll
|
||||
//! default to the binary's name.
|
||||
//! - `exposed.bin.architectures` is an array of the architectures that this binary is exposed
|
||||
//! for.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::Context;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::installable::manifest::InstallableManifestPackage;
|
||||
use crate::shared::Architecture;
|
||||
|
||||
pub mod manifest;
|
||||
|
||||
/// Bundles a package project into a Nye package installable.
|
||||
///
|
||||
/// Arguments:
|
||||
/// * `input` - The path to the package project directory. It must contain a `Nye.toml` manifest
|
||||
/// file and the files to bundle into the package.
|
||||
/// * `output` - The path to the output directory where the bundled package will be created.
|
||||
/// * `arch` - The architecture to bundle the package for. It must be one of the architectures
|
||||
/// specified in the `package.architectures` field of the `Nye.toml` manifest file.
|
||||
pub async fn bundle(input: impl AsRef<Path>, architecture: Architecture) -> anyhow::Result<()> {
|
||||
let input = input.as_ref();
|
||||
|
||||
let manifest = manifest::read(input.join("Nye.toml"))
|
||||
.await
|
||||
.context("Could not read the manifest of the project to bundle.")?;
|
||||
|
||||
manifest.validate()?;
|
||||
|
||||
if !manifest.package.architectures.contains(&architecture) {
|
||||
anyhow::bail!(
|
||||
concat!(
|
||||
"The architecture `{:?}` is not supported by the package project. The supported ",
|
||||
"architectures are: {:?}.",
|
||||
),
|
||||
architecture,
|
||||
manifest.package.architectures
|
||||
);
|
||||
}
|
||||
|
||||
let source_arch = input.join(architecture.to_string());
|
||||
let source_shared = input.join("shared");
|
||||
|
||||
let installable_manifest_package = InstallableManifestPackage {
|
||||
name: manifest.package.name,
|
||||
version: manifest.package.version,
|
||||
architecture,
|
||||
};
|
||||
|
||||
let source_arch_bin = source_arch.join("bin");
|
||||
let source_shared_bin = source_shared.join("bin");
|
||||
|
||||
let mut installable_manifest_exposed_bins = Vec::new();
|
||||
|
||||
let working_dir = tempfile::tempdir()
|
||||
.context("Could not create temporary working directory to bundle project.")?;
|
||||
let working_dir_path = working_dir.path();
|
||||
|
||||
for binary in manifest.exposed.binaries {
|
||||
if let Some(architectures) = binary.architectures {
|
||||
if !architectures.contains(&architecture) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let path = binary.path.as_ref().unwrap_or(&PathBuf::from(&binary.name));
|
||||
|
||||
let path_in_arch = source_arch_bin.join(&path);
|
||||
let path_in_shared = source_shared_bin.join(&path);
|
||||
|
||||
let path_in_arch_exists = fs::try_exists(path)
|
||||
.await
|
||||
.context("Could not check whether binary existed")?;
|
||||
let path_in_shared_exists = fs::try_exists(path)
|
||||
.await
|
||||
.context("Could not check whether binary existed")?;
|
||||
|
||||
if path_in_arch_exists {
|
||||
fs::copy(from, to)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A CPU architecture that a Nye package can support.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Architecture {
|
||||
X86,
|
||||
X86_64,
|
||||
Arm,
|
||||
Aarch64,
|
||||
M68k,
|
||||
Mips,
|
||||
Mips32r6,
|
||||
Mips64,
|
||||
Mips64r6,
|
||||
Csky,
|
||||
Powerpc,
|
||||
Powerpc64,
|
||||
Riscv32,
|
||||
Riscv64,
|
||||
S390x,
|
||||
Sparc,
|
||||
Sparc64,
|
||||
Hexagon,
|
||||
Loongarch32,
|
||||
Loongarch64,
|
||||
}
|
||||
|
||||
impl Display for Architecture {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Architecture::X86 => write!(f, "x86"),
|
||||
Architecture::X86_64 => write!(f, "x86_64"),
|
||||
Architecture::Arm => write!(f, "arm"),
|
||||
Architecture::Aarch64 => write!(f, "aarch64"),
|
||||
Architecture::M68k => write!(f, "m68k"),
|
||||
Architecture::Mips => write!(f, "mips"),
|
||||
Architecture::Mips32r6 => write!(f, "mips32r6"),
|
||||
Architecture::Mips64 => write!(f, "mips64"),
|
||||
Architecture::Mips64r6 => write!(f, "mips64r6"),
|
||||
Architecture::Csky => write!(f, "csky"),
|
||||
Architecture::Powerpc => write!(f, "powerpc"),
|
||||
Architecture::Powerpc64 => write!(f, "powerpc64"),
|
||||
Architecture::Riscv32 => write!(f, "riscv32"),
|
||||
Architecture::Riscv64 => write!(f, "riscv64"),
|
||||
Architecture::S390x => write!(f, "s390x"),
|
||||
Architecture::Sparc => write!(f, "sparc"),
|
||||
Architecture::Sparc64 => write!(f, "sparc64"),
|
||||
Architecture::Hexagon => write!(f, "hexagon"),
|
||||
Architecture::Loongarch32 => write!(f, "loongarch32"),
|
||||
Architecture::Loongarch64 => write!(f, "loongarch64"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "test-package"
|
||||
version = "0.1.0"
|
||||
description = "A package for testing the Nye package manager."
|
||||
architectures = ["x86", "x86_64"]
|
||||
|
||||
[[expose.bin]]
|
||||
name = "shared-binary"
|
||||
|
||||
[[expose.bin]]
|
||||
name = "exposed-binary"
|
||||
|
||||
[[expose.bin]]
|
||||
name = "exposed-binary-x86"
|
||||
architectures = ["x86"]
|
||||
|
||||
[[expose.bin]]
|
||||
name = "exposed-binary-x86_64"
|
||||
architectures = ["x86_64"]
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
echo "This binary is shared across all architectures."
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
echo "Hello from the test Nye package for the x86 architecture!"
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
echo "Hello from the test Nye package for the x86 architecture! This binary's name is arch-specific."
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
echo "This binary for the x86 architecture is not exposed."
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
echo "Hello from the test Nye package for the x86_64 architecture!"
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
echo "Hello from the test Nye package for the x86_64 architecture! This binary's name is arch-specific."
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
echo "This binary for the x86_64 architecture is not exposed."
|
||||
Reference in New Issue
Block a user