Checkpoint

This commit is contained in:
2026-08-02 18:30:34 +00:00
parent 596af4ed80
commit 6c33bcfb27
41 changed files with 6360 additions and 1 deletions
+12
View File
@@ -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"] }
+47
View File
@@ -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,
}
+3
View File
@@ -0,0 +1,3 @@
//! Manifest and utilities to handle package installables.
pub mod manifest;
+6
View File
@@ -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;
+135
View File
@@ -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)
}
+146
View File
@@ -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(())
}
+56
View File
@@ -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"),
}
}
}
+19
View File
@@ -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."