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
+31
View File
@@ -0,0 +1,31 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/rust
{
"name": "Rust",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/rust:2-1-trixie",
// Use 'mounts' to make the cargo cache persistent in a Docker Volume.
// "mounts": [
// {
// "source": "devcontainer-cargo-cache-${devcontainerId}",
// "target": "/usr/local/cargo",
// "type": "volume"
// }
// ]
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "rustup component add rustfmt --toolchain nightly",
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
"remoteUser": "root"
}
Generated
+3875
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
[workspace]
members = ["nye-terminal", "nye-daemon", "nye-wire-protocol", "nye-console", "nye-package"]
resolver = "3"
+1 -1
View File
@@ -1,3 +1,3 @@
# nye
A Linux package manager that keeps your system tidy.
A Linux package manager that keeps your system tidy.
+9
View File
@@ -0,0 +1,9 @@
[package]
name = "nye-console"
version = "0.1.0"
edition = "2024"
[dependencies]
colored = "3.1.1"
dialoguer = "0.12.0"
indicatif = { version = "0.18.6", features = ["tokio"] }
+305
View File
@@ -0,0 +1,305 @@
use std::fmt::Display;
use std::time::Duration;
use colored::Colorize;
use dialoguer::theme::Theme;
use dialoguer::{Input, Select};
use indicatif::{ProgressBar, ProgressStyle};
/// Takes a result and logs it if it's an error.
///
/// Arguments:
/// * `result` - The result.
pub fn catch_err<T, E>(result: Result<T, E>)
where
E: Display,
{
if let Err(error) = result {
err(error.to_string());
}
}
/// Displays a spinner loading bar on the console.
///
/// Arguments:
/// * `message` - The message to display with the spinner.
///
/// Returns:
/// [`ProgressBar`] - The spinner bar already being displayed on the console.
pub fn spinner(message: impl Into<String>) -> ProgressBar {
let bar = ProgressBar::new_spinner()
.with_message(message.into())
.with_style(
ProgressStyle::default_spinner()
.tick_strings(&["[\\]", "[|]", "[/]", "[-]", "^w^"])
.template("{spinner:.cyan} {msg}")
.unwrap(),
);
bar.enable_steady_tick(Duration::from_millis(200));
bar
}
/// Displays a bytes progress bar on the console.
///
/// Arguments:
/// * `message` - The message to display with the progress bar.
///
/// Returns:
/// [`ProgressBar`] - The progress bar with a ticking spinner.
pub fn progress_bytes(message: impl Into<String>, total: u64) -> ProgressBar {
let bar = ProgressBar::new(total)
.with_message(message.into())
.with_style(
ProgressStyle::default_bar()
.progress_chars("##")
.tick_strings(&["[\\]", "[|]", "[/]", "[-]", "^w^"])
.template(
"{spinner:.cyan} {wide_msg} {bar:.cyan/black} {binary_bytes}/{binary_total_bytes}",
)
.unwrap(),
);
bar.enable_steady_tick(Duration::from_millis(200));
bar
}
/// Displays a success log message.
///
/// Use this when finishing a task to indicate success.
///
/// Arguments:
/// * `message` - The message to log.
pub fn yay(message: impl Into<String>) {
let prefix = "YAY".cyan();
let message = message.into();
println!("{prefix} {message}");
}
/// Displays an info log message.
///
/// Arguments:
/// * `message` - The message to log.
pub fn info(message: impl Into<String>) {
let prefix = "III".blue();
let message = message.into();
println!("{prefix} {message}");
}
/// Displays a warning log message.
///
/// Arguments:
/// * `message` - The message to log.
pub fn warn(message: impl Into<String>) {
let prefix = "WWW".yellow();
let message = message.into();
println!("{prefix} {message}");
}
/// Displays an error log message.
///
/// Arguments:
/// * `message` - The message to log.
pub fn err(message: impl Into<String>) {
let prefix = "ERR".red();
let message = message.into();
println!("{prefix} {message}");
}
/// A theme for the dialoguer crate that is used to prompt the user for input and selection.
struct InputTheme;
impl Theme for InputTheme {
fn format_input_prompt(
&self,
f: &mut dyn std::fmt::Write,
prompt: &str,
_default: Option<&str>,
) -> std::fmt::Result {
let prefix = ">>>".purple();
write!(f, "{prefix} {prompt}")
}
fn format_input_prompt_selection(
&self,
f: &mut dyn std::fmt::Write,
prompt: &str,
sel: &str,
) -> std::fmt::Result {
let prefix = ">>>".purple();
write!(f, "{prefix} {prompt}{sel}")
}
fn format_select_prompt(&self, f: &mut dyn std::fmt::Write, prompt: &str) -> std::fmt::Result {
let prefix = ">>>".purple();
write!(f, "{prefix} {prompt}")
}
fn format_select_prompt_item(
&self,
f: &mut dyn std::fmt::Write,
text: &str,
active: bool,
) -> std::fmt::Result {
if active {
write!(f, "{}", format!(" * {text}").purple())
} else {
write!(f, " {text}")
}
}
fn format_select_prompt_selection(
&self,
f: &mut dyn std::fmt::Write,
prompt: &str,
sel: &str,
) -> std::fmt::Result {
let prefix = ">>>".purple();
write!(f, "{prefix} {prompt}{}", sel.purple())
}
}
/// Prompts the user for input.
///
/// Arguments:
/// * `prompt` - The message to prompt the user with.
///
/// Returns:
/// * `Ok(String)` - The user's input.
/// * `Err(Error)` - If the user cancelled the operation or if the user could not be prompted.
pub fn input(prompt: impl Into<String>) -> Result<String, dialoguer::Error> {
Input::with_theme(&InputTheme)
.with_prompt(prompt)
.interact_text()
}
/// Prompts the user for input with a default value.
///
/// Arguments:
/// * `prompt` - The message to prompt the user with.
/// * `default` - The default value.
///
/// Returns:
/// * `Ok(String)` - The user's input.
/// * `Err(Error)` - If the user cancelled the operation or if the user could not be prompted.
pub fn input_with_default(
prompt: impl Into<String>,
default: impl Into<String>,
) -> Result<String, dialoguer::Error> {
Input::with_theme(&InputTheme)
.with_prompt(prompt)
.default(default.into())
.interact_text()
}
/// Prompts the user to select an option.
///
/// Arguments:
/// * `prompt` - The message to prompt the user with.
/// * `options` - The options to give the user for selection.
///
/// Returns:
/// * `Ok(usize)` - The index of the option selected by the user.
/// * `Err(Error)` - If the user cancelled the operation or if the user could not be prompted.
pub fn select(
prompt: impl Into<String>,
options: impl IntoIterator<Item = impl ToString>,
) -> Result<usize, dialoguer::Error> {
Select::with_theme(&InputTheme)
.with_prompt(prompt)
.items(options)
.interact()
}
/// Prompts the user to select an option with an option selected by default.
///
/// Arguments:
/// * `prompt` - The message to prompt the user with.
/// * `options` - The options to give the user for selection.
/// * `default` - The index of the default option.
///
/// Returns:
/// * `Ok(usize)` - The index of the option selected by the user.
/// * `Err(Error)` - If the user cancelled the operation or if the user could not be prompted.
pub fn select_with_default(
prompt: impl Into<String>,
options: impl IntoIterator<Item = impl ToString>,
default: usize,
) -> Result<usize, dialoguer::Error> {
Select::with_theme(&InputTheme)
.with_prompt(prompt)
.items(options)
.default(default)
.interact()
}
/// Prompts the user to select an option with an option selected by default, without reporting the
/// selection.
///
/// Arguments:
/// * `prompt` - The message to prompt the user with.
/// * `options` - The options to give the user for selection.
/// * `default` - The index of the default option.
///
/// Returns:
/// * `Ok(usize)` - The index of the option selected by the user.
/// * `Err(Error)` - If the user cancelled the operation or if the user could not be prompted.
pub fn select_with_default_without_report(
prompt: impl Into<String>,
options: impl IntoIterator<Item = impl ToString>,
default: usize,
) -> Result<usize, dialoguer::Error> {
Select::with_theme(&InputTheme)
.with_prompt(prompt)
.items(options)
.default(default)
.report(false)
.interact()
}
/// Formats items into a human-readable list.
///
/// For example,
/// - `[1] => "1"`
/// - `[1, 2] => "1 and 2"`
/// - `[1, 2, 3] => "1, 2, and 3"`.
/// - `[1, 2, 3, 4] => "1, 2, 3, and 4"`.
///
/// Arguments:
/// * `items` - The items of the list.
///
/// Returns:
/// [`String`] -> The formatted list.
pub fn list<T>(items: &[T]) -> String
where
T: Display,
{
let mut string = String::new();
for (i, item) in items.iter().enumerate() {
let is_first = i == 0;
let is_penultimate = i == items.len() - 2;
let is_last = i == items.len() - 1;
match (is_first, is_penultimate, is_last) {
(false, false, false) => string.push_str(&format!("{item}, ")),
(false, false, true) => string.push_str(&item.to_string()),
(false, true, false) => string.push_str(&format!("{item}, and ")),
(false, true, true) => unreachable!(),
(true, false, false) => string.push_str(&format!("{item}, ")),
(true, false, true) => string.push_str(&item.to_string()),
(true, true, false) => string.push_str(&format!("{item} and ")),
(true, true, true) => unreachable!(),
}
}
string
}
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "nye-daemon"
version = "0.1.0"
edition = "2024"
[dependencies]
clap = { version = "4.6.5", features = ["wrap_help", "derive", "env"] }
tokio = { version = "1.53.1", features = ["full"] }
nye-wire-protocol = { version = "0.1.0", path = "../nye-wire-protocol" }
tracing = { version = "0.1.44", features = ["async-await"] }
anyhow = { version = "1.0.104", features = ["backtrace"] }
uzers = "0.12.2"
nye-console = { version = "0.1.0", path = "../nye-console" }
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
toasty = { version = "0.9.0", features = ["turso"] }
+8
View File
@@ -0,0 +1,8 @@
#[derive(Debug, clap::Parser)]
pub struct Args {
/// Whether to enable debug logging.
///
/// When enabled, the daemon will log additional information to help with debugging.
#[arg(short, long, default_value_t = false)]
pub debug: bool,
}
+1
View File
@@ -0,0 +1 @@
pub mod setup;
+84
View File
@@ -0,0 +1,84 @@
use std::path::PathBuf;
use anyhow::Context;
use nye_wire_protocol::messages::MessageResponse;
use nye_wire_protocol::{Connection, Stream};
use tokio::fs;
use crate::utils::LinuxUser;
pub async fn run(connection: &mut Connection, stream: &mut Stream) -> anyhow::Result<()> {
let user =
LinuxUser::from_connection(connection).context("Could not get the user to set up.")?;
setup(SetupKind::User(user.clone()))
.await
.context("Could not set up the user.")?;
stream
.send(MessageResponse::Setup)
.await
.context("Could not send setup completion notification to the client.")?;
tracing::trace!(
user.name,
user.uid,
user.gid,
"Sent setup completion notification to the client"
);
Ok(())
}
/// Represents the type of setup to perform.
pub enum SetupKind {
/// Sets up the Nye package manager for a specific user.
User(LinuxUser),
/// Sets up the Nye package manager for the entire system.
System,
}
impl SetupKind {
/// Returns the root directory for the setup kind.
///
/// Returns:
/// [`PathBuf`] - The root directory for the setup kind.
fn root(&self) -> PathBuf {
match self {
SetupKind::User(user) => format!("/usr/{}/nye", user.name).into(),
SetupKind::System => "/nye".into(),
}
}
}
/// Sets up the environment for the given setup kind.
///
/// Arguments:
/// * `kind` - The kind of setup to perform.
///
/// Returns:
/// * `Ok(())` - if the setup was successful.
/// * `Err(anyhow::Error)` - if there was an error during setup.
pub async fn setup(kind: SetupKind) -> anyhow::Result<()> {
let dirs = [
kind.root(),
kind.root().join("packages"),
kind.root().join("volumes"),
];
for dir in dirs {
fs::create_dir_all(&dir)
.await
.with_context(|| format!("Could not create directories at {}", dir.display()))?;
tracing::debug!("Created directories at {}", dir.display());
}
tracing::info!(
"The Nye package manager was successfully set up at {}",
kind.root().display()
);
Ok(())
}
View File
+106
View File
@@ -0,0 +1,106 @@
use anyhow::Context;
use nye_wire_protocol::Connection;
use nye_wire_protocol::messages::MessageRequest;
use tokio::fs;
use tokio::task::JoinSet;
use crate::commands;
use crate::utils::LinuxUser;
/// Listens for incoming connections from clients and handles them.
///
/// Arguments:
/// * `tasks` - A mutable reference to a `JoinSet` that will be used to spawn tasks for handling
/// incoming connections.
///
/// Returns:
/// * `!` - This function never returns, as it runs an infinite loop accepting incoming connections
/// and spawning tasks to handle them.
/// * `Err(anyhow::Error)` - if there was an error binding to the socket or accepting an incoming
/// connection.
pub async fn run(tasks: &mut JoinSet<anyhow::Result<()>>) -> anyhow::Result<()> {
let listener = nye_wire_protocol::listen("/run/nye-packages.sock")
.await
.context("Could not bind to the socket at /run/nye-packages.sock")?;
tracing::info!("Listening for incoming connections on /run/nye-packages.sock");
loop {
let connection = listener
.accept()
.await
.context("There was an error accepting an incoming connection.")?;
tasks.spawn(async move {
let result = handle_connection(connection).await;
if let Err(e) = &result {
eprintln!("{e}");
}
result
});
}
}
/// Handle a connection from a client.
///
/// Arguments:
/// * `connection` - The connection to handle.
///
/// Returns:
/// * `Ok(())` - if the connection was handled successfully.
/// * `Err(anyhow::Error)` - if there was an error handling the connection.
async fn handle_connection(mut connection: Connection) -> anyhow::Result<()> {
let user = LinuxUser::from_ucred(connection.peer_credentials())?;
tracing::info!(user.name, user.uid, user.gid, "Accepted a connection",);
let mut stream = connection
.recv_stream()
.await
.context("Could not receive an incoming stream from the client.")?;
tracing::info!(
user.name,
user.uid,
user.gid,
stream.id = stream.id(),
"Accepted a stream",
);
let request = stream
.recv_request()
.await
.context("Could not receive an incoming message from the incoming stream.")?;
tracing::info!(
user.name,
user.uid,
user.gid,
stream.id = stream.id(),
message.kind = ?request.kind(),
"Received a message"
);
match request {
MessageRequest::Setup => {
commands::setup::run(&mut connection, &mut stream).await?;
}
}
Ok(())
}
/// Clean up the socket file at `/run/nye-packages.sock`.
///
/// This function is called when the daemon is shutting down to remove the socket file.
///
/// Returns:
/// * `Ok(())` - if the socket file was removed successfully.
/// * `Err(anyhow::Error)` - if there was an error removing the socket file.
pub async fn cleanup() -> anyhow::Result<()> {
fs::remove_file("/run/nye-packages.sock")
.await
.context("Could not remove the socket at /run/nye-packages.sock")
}
+58
View File
@@ -0,0 +1,58 @@
use anyhow::Context;
use clap::Parser;
use tokio::signal;
use tokio::task::JoinSet;
use tracing_subscriber::EnvFilter;
use crate::args::Args;
use crate::commands::setup::SetupKind;
mod args;
mod commands;
mod db;
mod listener;
mod utils;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let args = Args::parse();
let user_id = uzers::get_current_uid();
if user_id != 0 {
anyhow::bail!(concat!(
"The daemon must be run as root, but the current user is not root. ",
"Please run the daemon as root."
));
}
let env_filter = if args.debug {
EnvFilter::new("off,nye_daemon=trace,nye_wire_protocol=trace")
} else {
EnvFilter::new("off,nye_daemon=info,nye_wire_protocol=warn")
};
tracing_subscriber::fmt()
.with_env_filter(env_filter)
.with_target(false)
.init();
commands::setup::setup(SetupKind::System)
.await
.context("Could not set up the Nye package manager's system-wide configuration.")?;
let mut tasks = JoinSet::new();
tokio::select! {
_ = listener::run(&mut tasks) => {}
_ = signal::ctrl_c() => {
tasks.join_all().await;
}
}
listener::cleanup()
.await
.context("Failed to cleanup socket listener.")?;
Ok(())
}
+51
View File
@@ -0,0 +1,51 @@
use anyhow::Context;
use nye_wire_protocol::Connection;
use tokio::net::unix::UCred;
/// A struct representing a Linux user.
#[derive(Clone)]
pub struct LinuxUser {
pub uid: u32,
pub gid: u32,
pub pid: i32,
pub name: String,
}
impl LinuxUser {
/// Creates a new [`LinuxUser`] from a [`UCred`].
///
/// Arguments:
/// * `ucred` - A reference to a [`UCred`] struct containing the user's credentials.
///
/// Returns:
/// * `Ok(LinuxUser)` - if the user was successfully created.
/// * `Err(anyhow::Error)` - if there was an error creating the user.
pub fn from_ucred(ucred: UCred) -> anyhow::Result<Self> {
let user =
uzers::get_user_by_uid(ucred.uid()).context("The UID of the user does not exist.")?;
Ok(Self {
uid: ucred.uid(),
gid: ucred.gid(),
pid: ucred
.pid()
.ok_or(anyhow::anyhow!("The client did not have a PID."))?,
name: user.name().to_string_lossy().to_string(),
})
}
/// Gets the [`LinuxUser`] of a [`Connection`].
///
/// Arguments:
/// * `connection` - A reference to a [`Connection`] struct containing the user's credentials.
///
/// Returns:
/// * `Ok(LinuxUser)` - if the user was successfully gotten.
/// * `Err(anyhow::Error)` - if there was an error getting the user.
pub fn from_connection(connection: &Connection) -> anyhow::Result<Self> {
let ucred = connection.peer_credentials();
Self::from_ucred(ucred)
}
}
+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."
+554
View File
@@ -0,0 +1,554 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "anyhow"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "bitflags"
version = "2.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
[[package]]
name = "bumpalo"
version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "bytes"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "clap"
version = "4.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
"terminal_size",
]
[[package]]
name = "clap_derive"
version = "4.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 3.0.3",
]
[[package]]
name = "clap_lex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "dotenvy"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "futures-core"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
[[package]]
name = "futures-task"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109"
[[package]]
name = "futures-util"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "js-sys"
version = "0.3.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
dependencies = [
"cfg-if",
"futures-util",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "libredox"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652"
dependencies = [
"libc",
]
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "mio"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
dependencies = [
"libc",
"wasi",
"windows-sys",
]
[[package]]
name = "nye"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"dotenvy",
"tokio",
"whoami",
]
[[package]]
name = "objc2-core-foundation"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags",
]
[[package]]
name = "objc2-system-configuration"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396"
dependencies = [
"objc2-core-foundation",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
]
[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "rustversion"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
[[package]]
name = "socket2"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "terminal_size"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
dependencies = [
"rustix",
"windows-sys",
]
[[package]]
name = "tokio"
version = "1.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys",
]
[[package]]
name = "tokio-macros"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.3",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasm-bindgen"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 2.0.119",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
version = "0.3.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "whoami"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d"
dependencies = [
"libc",
"libredox",
"objc2-system-configuration",
"web-sys",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "nye-terminal"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = { version = "1.0.104", features = ["backtrace"] }
clap = { version = "4.6.5", features = ["derive", "env", "wrap_help"] }
dotenvy = "0.15.7"
tokio = { version = "1.53.1", features = ["full"] }
whoami = { version = "2.1.2", default-features = false, features = ["std"] }
nye-wire-protocol = { version = "0.1.0", path = "../nye-wire-protocol" }
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
nye-console = { version = "0.1.0", path = "../nye-console" }
[[bin]]
name = "nye"
path = "src/main.rs"
+47
View File
@@ -0,0 +1,47 @@
//! CLI arguments for the terminal application.
use std::path::PathBuf;
#[derive(Debug, clap::Parser)]
pub struct Args {
/// Whether to display logs on the terminal.
#[arg(short, long, global = true, default_value_t = false)]
pub debug: bool,
#[clap(subcommand)]
pub command: Command,
}
#[derive(Debug, clap::Subcommand)]
pub enum Command {
/// Sets up the package manager for the current user or the system.
#[command(visible_alias = "s")]
Setup(SetupCommand),
/// Bundle the current project into a single package.
#[command(visible_alias = "b")]
Bundle(BundleCommand),
/// Install one or more packages.
#[command(visible_alias = "i")]
Install(InstallCommand),
}
#[derive(Debug, clap::Args)]
pub struct SetupCommand;
#[derive(Debug, clap::Args)]
pub struct BundleCommand {
#[arg(short, long, default_value = ".")]
path: PathBuf,
}
#[derive(Debug, clap::Args)]
pub struct InstallCommand {
/// The names of the packages to install.
packages: Vec<String>,
/// The path to the directory where the package is located.
#[arg(short, long)]
path: Option<PathBuf>,
}
+7
View File
@@ -0,0 +1,7 @@
use nye_wire_protocol::Connection;
use crate::args::BundleCommand;
pub async fn run(_command: &BundleCommand, connection: &Connection) -> anyhow::Result<()> {
Ok(())
}
+7
View File
@@ -0,0 +1,7 @@
use nye_wire_protocol::Connection;
use crate::args::InstallCommand;
pub async fn run(_command: &InstallCommand, connection: &Connection) -> anyhow::Result<()> {
Ok(())
}
+3
View File
@@ -0,0 +1,3 @@
pub mod bundle;
pub mod install;
pub mod setup;
+31
View File
@@ -0,0 +1,31 @@
use anyhow::Context;
use nye_wire_protocol::Connection;
use nye_wire_protocol::messages::MessageRequest;
use crate::args::SetupCommand;
pub async fn run(_command: &SetupCommand, connection: &Connection) -> anyhow::Result<()> {
let progress = nye_console::spinner(format!(
"Setting up the Nye package manager for {}...",
whoami::username().context("Could not get current user's username.")?
));
let mut stream = connection
.send_stream()
.await
.context("Could not open a stream to the daemon.")?;
stream
.send(MessageRequest::Setup)
.await
.context("Could not send setup request to the daemon.")?;
let _response = stream
.recv_response()
.await
.context("Could not receive response from the daemon.")?;
progress.finish_with_message("Done! The Nye package manager was setup for your user.");
Ok(())
}
+39
View File
@@ -0,0 +1,39 @@
use clap::Parser;
use tracing_subscriber::EnvFilter;
use crate::args::{Args, Command};
mod args;
mod commands;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
dotenvy::dotenv().ok();
let args = Args::parse();
if args.debug {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::new(
"off,nye_terminal=trace,nye_wire_protocol=trace",
))
.with_target(false)
.init();
}
let connection = nye_wire_protocol::connect("/run/nye-packages.sock").await?;
match &args.command {
Command::Setup(command) => {
commands::setup::run(command, &connection).await?;
}
Command::Bundle(command) => {
commands::bundle::run(command, &connection).await?;
}
Command::Install(command) => {
commands::install::run(command, &connection).await?;
}
}
Ok(())
}
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "nye-wire-protocol"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = { version = "1.0.104", features = ["backtrace"] }
bitcode = "0.6.9"
papaya = "0.2.4"
rustix = { version = "1.1.4", features = ["linux_latest", "net", "system"] }
tokio = { version = "1.53.1", features = ["io-util", "net"] }
tracing = { version = "0.1.44", features = ["async-await"] }
+550
View File
@@ -0,0 +1,550 @@
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use anyhow::Context;
use papaya::HashMap;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::unix::{OwnedReadHalf, OwnedWriteHalf, UCred};
use tokio::net::{UnixListener, UnixStream};
use tokio::sync::{Notify, mpsc};
use crate::messages::{Message, MessageRequest, MessageResponse, StreamMessage};
pub mod messages;
/// Binds a new [`UnixListener`] to the specified path.
///
/// Arguments:
/// * `path` - The path to bind the UnixListener to.
///
/// Returns:
/// * `Ok(Self)` - If the binding was successful, returns a new instance of [`Listener`].
/// * `Err(anyhow::Error)` - If the binding failed, returns an error.
pub async fn listen(path: impl AsRef<Path>) -> anyhow::Result<Listener> {
let listener = UnixListener::bind(path.as_ref()).context(format!(
"Could not listen for incoming connections at {}.",
path.as_ref().display()
))?;
tracing::info!(
"Listening for incoming connections at {}",
path.as_ref().display()
);
Ok(Listener { listener })
}
/// Connects to a Unix socket at the specified path and returns a new instance of [`Connection`].
///
/// Arguments:
/// * `path` - The path to the Unix socket to connect to.
///
/// Returns:
/// * `Ok(Connection)` - If the connection was successful, returns a new instance of
/// [`Connection`].
/// * `Err(anyhow::Error)` - If the connection failed, returns an error.
pub async fn connect(path: impl AsRef<Path>) -> anyhow::Result<Connection> {
let stream = UnixStream::connect(path.as_ref()).await.context(format!(
"Could not connect to the Unix socket at {}.",
path.as_ref().display()
))?;
tracing::info!(
"Connected to the Unix socket at {}",
path.as_ref().display()
);
Connection::new(stream, PartyType::Client)
}
/// A wrapper around a [`UnixListener`] to accept incoming connections on a Unix socket.
pub struct Listener {
listener: UnixListener,
}
impl Listener {
/// Accepts an incoming connection on the Unix socket.
///
/// Returns:
/// * `Ok(Connection)` - If the connection was accepted successfully, returns a new instance of
/// [`Connection`].
/// * `Err(anyhow::Error)` - If the connection could not be accepted, returns an error.
pub async fn accept(&self) -> anyhow::Result<Connection> {
let (stream, _) = self.listener.accept().await?;
Connection::new(stream, PartyType::Server)
}
}
/// An enum representing the type of party (client or server) in a connection.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PartyType {
Client,
Server,
}
impl PartyType {
/// Creates a new instance of [`PartyType`] from a given ID.
///
/// Arguments:
/// * `id` - The ID to derive the party type from.
///
/// Returns:
/// [`PartyType`] - A new instance of [`PartyType`] derived from the given ID.
fn from_id(id: u64) -> Self {
// The most significant bit of the ID indicates the party type.
if id & (1 << 63) != 0 {
PartyType::Server
} else {
PartyType::Client
}
}
/// Converts a given ID to a party-specific ID based on the party type.
///
/// Arguments:
/// * `id` - The ID to convert to a party-specific ID.
///
/// Returns:
/// [`u64`] - The party-specific ID derived from the given ID.
fn type_id(&self, id: u64) -> u64 {
match self {
PartyType::Client => id & !(1 << 63),
PartyType::Server => id | (1 << 63),
}
}
}
type StreamsMap = Arc<HashMap<u64, mpsc::UnboundedSender<Message>>>;
type PendingStreamsTx = mpsc::UnboundedSender<Stream>;
type PendingStreamsRx = mpsc::UnboundedReceiver<Stream>;
/// A wrapper around a [`UnixStream`] to represent a connection to a Unix socket.
pub struct Connection {
/// The type of party (client or server) of the current party.
party_type: PartyType,
/// A shared map of protocol streams to route incoming messages to.
streams: StreamsMap,
/// The peer credentials of the other party in the connection.
peer_credentials: UCred,
/// An atomic counter to generate unique stream IDs for each protocol stream.
stream_id_counter: AtomicU64,
/// Notifies when the connection is closing, allowing tasks to clean up resources.
closing: Arc<Notify>,
/// A channel to receive pending streams that need to be processed by this party.
pending_streams: PendingStreamsRx,
/// A channel to send outgoing messages to the other party.
outgoing_tx: mpsc::UnboundedSender<StreamMessage>,
}
impl Connection {
/// Creates a new instance of [`Connection`] from an existing [`UnixStream`].
///
/// Arguments:
/// * `party_type` - The type of party (client or server) that this connection represents.
/// * `stream` - The existing [`UnixStream`] to wrap in a [`Connection`].
///
/// Returns:
/// * `Self` - A new instance of [`Connection`] wrapping the provided [`UnixStream`].
fn new(stream: UnixStream, party_type: PartyType) -> anyhow::Result<Self> {
let peer_credentials = stream
.peer_cred()
.context("Could not get the credentials of the connection's peer.")?;
let (stream_rx, stream_tx) = stream.into_split();
let streams: StreamsMap = Default::default();
let (outgoing_tx, outgoing_rx) = mpsc::unbounded_channel();
let (pending_streams_tx, pending_streams_rx) = mpsc::unbounded_channel();
let closing = Arc::new(Notify::new());
tokio::spawn({
let stream_rx = stream_rx;
let streams = streams.clone();
let pending_streams_tx = pending_streams_tx.clone();
let outgoing_tx = outgoing_tx.clone();
let closing = closing.clone();
async move {
let result = Self::route_incoming(
stream_rx,
streams,
pending_streams_tx,
party_type,
outgoing_tx,
closing,
)
.await;
if let Err(e) = &result {
tracing::warn!("{e:?}");
}
result
}
});
tokio::spawn(async move {
let result = Self::route_outgoing(stream_tx, outgoing_rx).await;
if let Err(e) = &result {
tracing::warn!("{e:?}");
}
result
});
Ok(Self {
party_type,
peer_credentials,
streams,
stream_id_counter: AtomicU64::new(0),
closing,
pending_streams: pending_streams_rx,
outgoing_tx,
})
}
/// Receives a pending stream that needs to be processed by this party.
///
/// Returns:
/// * `Ok(Stream)` - If a pending stream was received successfully, returns the stream.
/// * `Err(anyhow::Error)` - If the pending streams channel was closed, returns an error.
pub async fn recv_stream(&mut self) -> anyhow::Result<Stream> {
let stream = self
.pending_streams
.recv()
.await
.context("The pending streams channel was closed, which should not happen.")?;
Ok(stream)
}
/// Creates a new protocol stream with a unique ID and returns it.
///
/// Returns:
/// * `Ok(Stream)` - If the stream was created successfully, returns the new stream.
/// * `Err(anyhow::Error)` - If an error occurred while creating the stream, returns an error.
pub async fn send_stream(&self) -> anyhow::Result<Stream> {
let id = self.stream_id_counter.fetch_add(1, Ordering::Relaxed);
let id = self.party_type.type_id(id);
let (incoming_tx, incoming_rx) = mpsc::unbounded_channel();
let stream = Stream::new(
id,
self.streams.clone(),
incoming_tx,
incoming_rx,
self.outgoing_tx.clone(),
);
self.streams.pin().insert(id, stream.incoming_tx.clone());
Ok(stream)
}
/// Routes incoming messages from the Unix stream to the appropriate protocol streams.
///
/// Arguments:
/// * `stream_rx` - The read half of the Unix stream to read incoming messages from.
/// * `streams` - A shared map of protocol streams to route incoming messages to.
/// * `pending` - A channel to send pending streams that need to be processed by this party.
/// * `party_type` - The party type of the current party.
/// * `outgoing_tx` - A channel to send outgoing messages to the other party.
/// * `closing` - A notification to signal when the connection is closing.
///
/// Returns:
/// * `!` - This function runs indefinitely and does not return.
/// * `Err(anyhow::Error)` - If an error occurs while routing incoming messages, returns an
/// error.
async fn route_incoming(
mut stream_rx: OwnedReadHalf,
streams: StreamsMap,
pending: PendingStreamsTx,
party_type: PartyType,
outgoing_tx: mpsc::UnboundedSender<StreamMessage>,
closing: Arc<Notify>,
) -> anyhow::Result<()> {
loop {
// Only close on message boundaries.
let message_size = tokio::select! {
_ = closing.notified() => {
// The connection is closing, so we should stop routing incoming messages and
// return.
return Ok(());
}
size = stream_rx.read_u32() => {
size.context("Could not read the size of the incoming message.")?
}
};
let mut message_data = vec![0u8; message_size as usize];
stream_rx
.read_exact(&mut message_data)
.await
.context("Could not read the data of the incoming message.")?;
let message: StreamMessage =
bitcode::decode(&message_data).context("Could not decode the incoming message.")?;
tracing::info!(
stream.id = message.stream_id,
message.kind = ?message.data.kind(),
"Received a message"
);
if let Some(stream) = streams.pin().get(&message.stream_id) {
tracing::trace!(
stream.id = message.stream_id,
message.kind = ?message.data.kind(),
"Routing the message to the existing stream"
);
stream.send(message.data).context(
"Could not send the message through the stream's incoming messages channel.",
)?;
} else if PartyType::from_id(message.stream_id) != party_type {
// The other party initiated this stream, so we need to create a new stream and
// send it to the pending channel.
tracing::trace!(
stream.id = message.stream_id,
message.kind = ?message.data.kind(),
"Creating a new stream for the incoming message"
);
let (incoming_tx, incoming_rx) = mpsc::unbounded_channel();
let stream = Stream::new(
message.stream_id,
streams.clone(),
incoming_tx.clone(),
incoming_rx,
outgoing_tx.clone(),
);
stream.incoming_tx.send(message.data).context(concat!(
"Could not send the first message of the stream through the incoming ",
"messages channel."
))?;
streams.pin().insert(message.stream_id, incoming_tx);
pending
.send(stream)
.context("Could not send the stream through the pending streams channel.")?;
} else {
anyhow::bail!(concat!(
"A message was received from the other party for a stream that does not ",
"exist. The ID said we created the stream, yet we did not or it was already ",
"closed. This is a bug, either on this side or the other side (daemon or ",
"client)."
));
}
}
}
/// Routes outgoing messages from the protocol streams to the Unix stream.
///
/// Arguments:
/// * `stream_tx` - The write half of the Unix stream to write outgoing messages to.
/// * `outgoing_rx` - A channel to receive outgoing messages from the protocol streams.
///
/// Returns:
/// * `!` - This function runs indefinitely and does not return.
/// * `Err(anyhow::Error)` - If an error occurs while routing outgoing messages, returns an
/// error.
async fn route_outgoing(
mut stream_tx: OwnedWriteHalf,
mut outgoing_rx: mpsc::UnboundedReceiver<StreamMessage>,
) -> anyhow::Result<()> {
loop {
// If closed, the connection was dropped.
let Some(message) = outgoing_rx.recv().await else {
return Ok(());
};
let message_data = bitcode::encode(&message);
let message_size = message_data.len() as u32;
stream_tx
.write_u32(message_size)
.await
.context("Could not send outgoing message's size through the socket.")?;
stream_tx
.write_all(&message_data)
.await
.context("Could not send outgoing message's data through the socket.")?;
tracing::info!(
stream.id = message.stream_id,
message.kind = ?message.data.kind(),
"Sent a message"
);
}
}
/// Returns the peer credentials of the other party in the connection.
///
/// Returns:
/// [`UCred`] - A reference to the peer credentials of the other party in the connection.
pub fn peer_credentials(&self) -> UCred {
self.peer_credentials
}
}
impl Drop for Connection {
fn drop(&mut self) {
let peer_credentials = self.peer_credentials();
tracing::debug!(
user.uid = peer_credentials.uid(),
user.gid = peer_credentials.gid(),
user.pid = peer_credentials.pid(),
"Closing the connection"
);
self.closing.notify_waiters();
}
}
/// A representation of a protocol stream that can send and receive messages over a connection.
pub struct Stream {
id: u64,
streams: StreamsMap,
incoming_tx: mpsc::UnboundedSender<Message>,
incoming_rx: mpsc::UnboundedReceiver<Message>,
outgoing_tx: mpsc::UnboundedSender<StreamMessage>,
}
impl Stream {
/// Creates a new instance of [`Stream`] with the specified ID.
///
/// Arguments:
/// * `id` - The ID to assign to the new stream.
/// * `streams` - A shared map of protocol streams to route incoming messages to.
/// * `incoming_rx` - A channel to receive incoming messages from the other party.
/// * `incoming_tx` - A channel to receive incoming messages from the other party.
/// * `outgoing_tx` - A channel to send outgoing messages to the other party.
///
/// Returns:
/// [`Stream`] - A new instance of [`Stream`] with the specified ID.
fn new(
id: u64,
streams: StreamsMap,
incoming_tx: mpsc::UnboundedSender<Message>,
incoming_rx: mpsc::UnboundedReceiver<Message>,
outgoing_tx: mpsc::UnboundedSender<StreamMessage>,
) -> Self {
Self {
id,
streams,
incoming_tx,
incoming_rx,
outgoing_tx,
}
}
/// Sends a message over the stream to the other party.
///
/// Arguments:
/// * `message` - The message to send over the stream.
///
/// Returns:
/// * `Ok(())` - If the message was sent successfully.
/// * `Err(anyhow::Error)` - If an error occurred while sending the message.
pub async fn send(&self, message: impl Into<Message>) -> anyhow::Result<()> {
let stream_message = StreamMessage {
stream_id: self.id,
data: message.into(),
};
self.outgoing_tx
.send(stream_message)
.context("The outgoing messages channel was closed.")?;
Ok(())
}
/// Receives a message from the stream sent by the other party.
///
/// Returns:
/// * `Ok(Message)` - If a message was received successfully.
/// * `Err(anyhow::Error)` - If an error occurred while receiving the message.
pub async fn recv(&mut self) -> anyhow::Result<Message> {
let message = self.incoming_rx.recv().await.context(
"The incoming messages channel for this stream was closed, which should not happen.",
)?;
Ok(message)
}
/// Receives a request message from the stream sent by the other party.
///
/// Returns:
/// * `Ok(MessageRequest)` - If a request message was received successfully.
/// * `Err(anyhow::Error)` - If an error occurred while receiving the request message or the
/// message received was a response.
pub async fn recv_request(&mut self) -> anyhow::Result<MessageRequest> {
let message = self.recv().await?;
match message {
Message::Request(request) => Ok(request),
_ => anyhow::bail!(concat!(
"The message received from the other party was not a request. ",
"This is a bug, either on this side or the other side (daemon or client)."
)),
}
}
/// Receives a response message from the stream sent by the other party.
///
/// Returns:
/// * `Ok(MessageResponse)` - If a response message was received successfully.
/// * `Err(anyhow::Error)` - If an error occurred while receiving the response message or the
/// message received was a request.
pub async fn recv_response(&mut self) -> anyhow::Result<MessageResponse> {
let message = self.recv().await?;
match message {
Message::Response(response) => Ok(response),
_ => anyhow::bail!(concat!(
"The message received from the other party was not a response. ",
"This is a bug, either on this side or the other side (daemon or client)."
)),
}
}
/// Returns the ID of the stream.
///
/// Returns:
/// [`u64`] - The ID of the stream.
pub fn id(&self) -> u64 {
self.id
}
}
impl Drop for Stream {
fn drop(&mut self) {
let id = self.id;
tracing::debug!(
stream.id = id,
"Closing the stream and removing it from the streams map."
);
self.streams.pin().remove(&id);
}
}
+105
View File
@@ -0,0 +1,105 @@
use bitcode::{Decode, Encode};
/// Represents a message in the Nye wire protocol.
#[derive(Debug, Clone, Encode, Decode)]
pub struct StreamMessage {
/// The unique identifier of the stream, used to correlate requests and responses.
pub stream_id: u64,
/// The type of the message, represented as a string.
pub data: Message,
}
/// Represents the data contained in a message in the Nye wire protocol.
#[derive(Debug, Clone, Encode, Decode)]
pub enum Message {
/// Represents a request message in the Nye wire protocol.
Request(MessageRequest),
/// Represents a response message in the Nye wire protocol.
Response(MessageResponse),
}
impl Message {
/// Returns the kind of the message.
///
/// Returns:
/// [`MessageKind`] - the kind of the message.
pub fn kind(&self) -> MessageKind {
match self {
Message::Request(request) => MessageKind::Request(request.kind()),
Message::Response(response) => MessageKind::Response(response.kind()),
}
}
}
/// Represents the kind of a message in the Nye wire protocol.
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
pub enum MessageKind {
Request(MessageRequestKind),
Response(MessageResponseKind),
}
/// Represents a request message in the Nye wire protocol.
///
/// Only sent from the client to the daemon.
#[derive(Debug, Clone, Encode, Decode)]
pub enum MessageRequest {
Setup,
}
impl MessageRequest {
/// Returns the kind of the message request.
///
/// Returns:
/// [`MessageRequestKind`] - the kind of the message request.
pub fn kind(&self) -> MessageRequestKind {
match self {
MessageRequest::Setup => MessageRequestKind::Setup,
}
}
}
impl From<MessageRequest> for Message {
fn from(request: MessageRequest) -> Self {
Message::Request(request)
}
}
/// Represents the kind of a message request in the Nye wire protocol.
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
pub enum MessageRequestKind {
Setup,
}
/// Represents a response message in the Nye wire protocol.
///
/// Only sent from the daemon to the client.
#[derive(Debug, Clone, Encode, Decode)]
pub enum MessageResponse {
Setup,
}
impl MessageResponse {
/// Returns the kind of the message response.
///
/// Returns:
/// [`MessageResponseKind`] - the kind of the message response.
pub fn kind(&self) -> MessageResponseKind {
match self {
MessageResponse::Setup => MessageResponseKind::SetupFinished,
}
}
}
impl From<MessageResponse> for Message {
fn from(response: MessageResponse) -> Self {
Message::Response(response)
}
}
/// Represents the kind of a message response in the Nye wire protocol.
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
pub enum MessageResponseKind {
SetupFinished,
}
+2
View File
@@ -0,0 +1,2 @@
group_imports = "StdExternalCrate"
imports_granularity = "Module"