[BROKEN!] Refactor tasks to allow dynamic loading and executing
parent
23728d05ff
commit
a8426cb4f9
@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://getcryst.al/config.schema.json",
|
||||
"properties": {
|
||||
"enable_flatpak": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enable_timeshift": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enable_zramd": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
[distro]
|
||||
name = "Crystal Linux"
|
||||
website = "https://getcryst.al"
|
||||
|
||||
[config]
|
||||
schema = "config.schema.json"
|
||||
|
||||
[tasks]
|
||||
|
||||
[tasks.enable_flatpak]
|
||||
config_field = "enable_flatpak"
|
||||
|
||||
[tasks.enable_timeshift]
|
||||
config_field = "enable_timeshift"
|
||||
|
||||
[tasks.enable_zramd]
|
||||
config_field = "enable_zramd"
|
@ -0,0 +1,44 @@
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
/// The config file of a distro that defines
|
||||
/// how that distro should be installed
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Config {
|
||||
/// Metadata about the distro
|
||||
pub distro: DistroMetadata,
|
||||
|
||||
/// Configuration related to the Operating system
|
||||
/// setup configuration
|
||||
pub config: OSConfigMetadata,
|
||||
|
||||
/// Additional distro specific tasks
|
||||
pub tasks: HashMap<String, TaskConfig>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct DistroMetadata {
|
||||
/// The name of the distro
|
||||
pub name: String,
|
||||
|
||||
/// The website of the distro
|
||||
pub website: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct OSConfigMetadata {
|
||||
/// The path of the config schema file
|
||||
pub schema: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// The configuration of a single task
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct TaskConfig {
|
||||
/// The name of the config field
|
||||
pub config_field: String,
|
||||
|
||||
/// If the task should be skipped if the
|
||||
/// config value of that task is null
|
||||
pub skip_on_null_config: bool,
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
mod config;
|
||||
pub mod config;
|
||||
mod os_config;
|
||||
|
||||
pub use config::*;
|
||||
pub use os_config::*;
|
||||
|
@ -0,0 +1,20 @@
|
||||
use super::{BaseTask, Task};
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref ALL_BASE_TASKS: Vec<Task> = get_all_base_tasks();
|
||||
}
|
||||
|
||||
fn get_all_base_tasks() -> Vec<Task> {
|
||||
vec![
|
||||
Task::Base(BaseTask::ConfigureLocale),
|
||||
Task::Base(BaseTask::ConfigureNetwork),
|
||||
Task::Base(BaseTask::CreatePartitions),
|
||||
Task::Base(BaseTask::InstallBase),
|
||||
Task::Base(BaseTask::InstallBootloader),
|
||||
Task::Base(BaseTask::InstallDesktop),
|
||||
Task::Base(BaseTask::InstallExtraPackages),
|
||||
Task::Base(BaseTask::SetupRootUser),
|
||||
Task::Base(BaseTask::SetupUsers),
|
||||
]
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use embed_nu::{Argument, CommandGroupConfig, Context, ValueIntoExpression};
|
||||
use tokio::fs;
|
||||
|
||||
use crate::{
|
||||
distro::OSConfig,
|
||||
error::{AppError, AppResult},
|
||||
};
|
||||
|
||||
pub struct ExecBuilder {
|
||||
pub script: PathBuf,
|
||||
pub os_config: OSConfig,
|
||||
pub task_config: embed_nu::Value,
|
||||
}
|
||||
|
||||
impl ExecBuilder {
|
||||
pub async fn exec(self) -> AppResult<()> {
|
||||
let script_contents = self.get_script_contents().await?;
|
||||
let mut ctx = Context::builder()
|
||||
.with_command_groups(CommandGroupConfig::default().all_groups(true))?
|
||||
.add_var("TRM_CONFIG", self.os_config)?
|
||||
.add_script(script_contents)?
|
||||
.build()?;
|
||||
if ctx.has_fn("main") {
|
||||
let pipeline = ctx.call_fn(
|
||||
"main",
|
||||
vec![Argument::Positional(self.task_config.into_expression())],
|
||||
)?;
|
||||
ctx.print_pipeline_stderr(pipeline)?;
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(AppError::MissingMain(self.script))
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_script_contents(&self) -> AppResult<String> {
|
||||
let contents = fs::read_to_string(&self.script).await?;
|
||||
|
||||
Ok(contents)
|
||||
}
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use embed_nu::IntoValue;
|
||||
|
||||
use crate::{distro::OSConfig, error::AppResult};
|
||||
|
||||
use self::exec_builder::ExecBuilder;
|
||||
pub mod base_tasks;
|
||||
pub mod exec_builder;
|
||||
pub mod task_executor;
|
||||
|
||||
pub trait TaskTrait {
|
||||
fn up(&self, config: &OSConfig) -> AppResult<ExecBuilder>;
|
||||
fn down(&self, config: &OSConfig) -> AppResult<ExecBuilder>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Task {
|
||||
Base(BaseTask),
|
||||
Custom(CustomTask),
|
||||
}
|
||||
|
||||
impl TaskTrait for Task {
|
||||
#[inline]
|
||||
fn up(&self, config: &OSConfig) -> AppResult<ExecBuilder> {
|
||||
match self {
|
||||
Task::Base(b) => b.up(config),
|
||||
Task::Custom(c) => c.up(config),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn down(&self, config: &OSConfig) -> AppResult<ExecBuilder> {
|
||||
match self {
|
||||
Task::Base(b) => b.down(config),
|
||||
Task::Custom(c) => c.down(config),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum BaseTask {
|
||||
ConfigureLocale,
|
||||
ConfigureNetwork,
|
||||
CreatePartitions,
|
||||
InstallBase,
|
||||
InstallBootloader,
|
||||
InstallDesktop,
|
||||
InstallExtraPackages,
|
||||
SetupRootUser,
|
||||
SetupUsers,
|
||||
}
|
||||
|
||||
impl BaseTask {
|
||||
fn config_key(&self) -> Option<&'static str> {
|
||||
let field = match self {
|
||||
BaseTask::ConfigureLocale => "locale",
|
||||
BaseTask::ConfigureNetwork => "network",
|
||||
BaseTask::CreatePartitions => "partitions",
|
||||
BaseTask::InstallBootloader => "bootloader",
|
||||
BaseTask::InstallDesktop => "desktop",
|
||||
BaseTask::InstallExtraPackages => "extra_packages",
|
||||
BaseTask::SetupRootUser => "root_user",
|
||||
BaseTask::SetupUsers => "users",
|
||||
_ => return None,
|
||||
};
|
||||
Some(field)
|
||||
}
|
||||
|
||||
fn task_name(&self) -> &'static str {
|
||||
match self {
|
||||
BaseTask::ConfigureLocale => "configure-locale",
|
||||
BaseTask::ConfigureNetwork => "configure-network",
|
||||
BaseTask::CreatePartitions => "create-partitions",
|
||||
BaseTask::InstallBase => "install-base",
|
||||
BaseTask::InstallBootloader => "install-bootloader",
|
||||
BaseTask::InstallDesktop => "install-desktop",
|
||||
BaseTask::InstallExtraPackages => "install-extra-packages",
|
||||
BaseTask::SetupRootUser => "setup-root-user",
|
||||
BaseTask::SetupUsers => "setup-users",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TaskTrait for BaseTask {
|
||||
fn up(&self, config: &OSConfig) -> AppResult<ExecBuilder> {
|
||||
let script = PathBuf::from(self.task_name()).join("up.nu");
|
||||
|
||||
let task_config = if let Some(key) = self.config_key() {
|
||||
config.get_nu_value(key)?
|
||||
} else {
|
||||
Option::<()>::None.into_value()
|
||||
};
|
||||
|
||||
Ok(ExecBuilder {
|
||||
script,
|
||||
os_config: config.to_owned(),
|
||||
task_config,
|
||||
})
|
||||
}
|
||||
|
||||
fn down(&self, config: &OSConfig) -> AppResult<ExecBuilder> {
|
||||
let script = PathBuf::from(self.task_name()).join("down.nu");
|
||||
let task_config = if let Some(key) = self.config_key() {
|
||||
config.get_nu_value(key)?
|
||||
} else {
|
||||
Option::<()>::None.into_value()
|
||||
};
|
||||
|
||||
Ok(ExecBuilder {
|
||||
script,
|
||||
os_config: config.to_owned(),
|
||||
task_config,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CustomTask {
|
||||
config_key: String,
|
||||
up_script: PathBuf,
|
||||
down_script: PathBuf,
|
||||
}
|
||||
|
||||
impl TaskTrait for CustomTask {
|
||||
fn up(&self, config: &OSConfig) -> AppResult<ExecBuilder> {
|
||||
let task_config = config.get_nu_value(&self.config_key)?;
|
||||
|
||||
Ok(ExecBuilder {
|
||||
script: self.up_script.to_owned(),
|
||||
os_config: config.to_owned(),
|
||||
task_config,
|
||||
})
|
||||
}
|
||||
|
||||
fn down(&self, config: &OSConfig) -> AppResult<ExecBuilder> {
|
||||
let task_config = config.get_nu_value(&self.config_key)?;
|
||||
|
||||
Ok(ExecBuilder {
|
||||
script: self.down_script.to_owned(),
|
||||
os_config: config.to_owned(),
|
||||
task_config,
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
use crate::distro::OSConfig;
|
||||
|
||||
use super::{base_tasks::ALL_BASE_TASKS, Task};
|
||||
|
||||
pub struct TaskExecutor {
|
||||
config: OSConfig,
|
||||
tasks: Vec<Task>,
|
||||
}
|
||||
|
||||
impl TaskExecutor {
|
||||
pub fn new(config: OSConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
tasks: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_base_tasks(&mut self) -> &mut Self {
|
||||
let mut base_tasks = (*ALL_BASE_TASKS).clone();
|
||||
self.tasks.append(&mut base_tasks);
|
||||
|
||||
self
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
use embed_nu::rusty_value::*;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::task;
|
||||
|
||||
task!(ConfigureLocale {
|
||||
name = "configure-locale"
|
||||
args = LocaleConfig
|
||||
});
|
||||
|
||||
#[derive(Clone, Deserialize, RustyValue, Debug)]
|
||||
pub struct LocaleConfig {
|
||||
pub locale: Vec<String>,
|
||||
pub keymap: String,
|
||||
pub timezone: String,
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
use embed_nu::rusty_value::*;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::task;
|
||||
|
||||
task!(ConfigureNetwork {
|
||||
name = "configure-network"
|
||||
args = NetworkConfig
|
||||
});
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, RustyValue)]
|
||||
pub struct NetworkConfig {
|
||||
pub hostname: String,
|
||||
pub ipv6_loopback: bool,
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use embed_nu::rusty_value::*;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::task;
|
||||
|
||||
task!(ConfigureUnakite {
|
||||
name = "configure-unakite"
|
||||
args = UnakiteConfig
|
||||
});
|
||||
|
||||
#[derive(Clone, Debug, RustyValue, Deserialize)]
|
||||
pub struct UnakiteConfig {
|
||||
pub root: PathBuf,
|
||||
pub old_root: PathBuf,
|
||||
pub efidir: PathBuf,
|
||||
pub bootdev: PathBuf,
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use embed_nu::rusty_value::*;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::task;
|
||||
|
||||
task!(CreatePartitions {
|
||||
name = "create-partitions"
|
||||
args = PartitionsConfig
|
||||
});
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, RustyValue)]
|
||||
pub struct PartitionsConfig {
|
||||
pub device: PathBuf,
|
||||
pub efi_partition: bool,
|
||||
pub partitions: Partitions,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, RustyValue)]
|
||||
pub enum Partitions {
|
||||
Auto,
|
||||
Manual(Vec<Partition>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, RustyValue)]
|
||||
pub struct Partition {
|
||||
pub mountpoint: PathBuf,
|
||||
pub blockdevice: PathBuf,
|
||||
pub filesystem: Option<FileSystem>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, RustyValue)]
|
||||
pub enum FileSystem {
|
||||
VFAT,
|
||||
BFS,
|
||||
CramFS,
|
||||
Ext2,
|
||||
Ext3,
|
||||
Ext4,
|
||||
FAT,
|
||||
MSDOS,
|
||||
XFS,
|
||||
BTRFS,
|
||||
Minix,
|
||||
F2FS,
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
use crate::task;
|
||||
|
||||
task!(InstallBase {
|
||||
name = "install-base"
|
||||
args = InstallBaseArgs
|
||||
});
|
||||
|
||||
type InstallBaseArgs = ();
|
@ -1,23 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use embed_nu::rusty_value::*;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::task;
|
||||
|
||||
task!(InstallBootloader {
|
||||
name = "install-bootloader"
|
||||
args = BootloaderConfig
|
||||
});
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, RustyValue)]
|
||||
pub struct BootloaderConfig {
|
||||
pub preset: BootloaderPreset,
|
||||
pub location: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, RustyValue)]
|
||||
pub enum BootloaderPreset {
|
||||
GrubEfi,
|
||||
Legacy,
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
use embed_nu::rusty_value::*;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::task;
|
||||
|
||||
task!(InstallDesktop {
|
||||
name = "install-desktop"
|
||||
args = DesktopConfig
|
||||
});
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, RustyValue)]
|
||||
pub enum DesktopConfig {
|
||||
Onyx,
|
||||
KdePlasma,
|
||||
Mate,
|
||||
Gnome,
|
||||
Cinnamon,
|
||||
Xfce,
|
||||
Budgie,
|
||||
Enlightenment,
|
||||
Lxqt,
|
||||
Sway,
|
||||
I3Gaps,
|
||||
HerbstluftWM,
|
||||
AwesomeWM,
|
||||
BSPWM,
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
use crate::task;
|
||||
|
||||
task!(InstallExtraPackages {
|
||||
name = "install-extra-packages"
|
||||
args = ExtraPackages
|
||||
});
|
||||
|
||||
pub type ExtraPackages = Vec<String>;
|
@ -1,8 +0,0 @@
|
||||
use crate::task;
|
||||
|
||||
task!(InstallFlatpak {
|
||||
name = "install-flatpak"
|
||||
args = FlatpakConfig
|
||||
});
|
||||
|
||||
pub type FlatpakConfig = ();
|
@ -1,18 +0,0 @@
|
||||
use embed_nu::rusty_value::*;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::task;
|
||||
|
||||
task!(InstallKernels {
|
||||
name = "install-kernels"
|
||||
args = KernelConfig
|
||||
});
|
||||
|
||||
#[derive(Clone, Debug, RustyValue, Deserialize)]
|
||||
pub struct KernelConfig {
|
||||
pub default: Kernel,
|
||||
pub additional: Vec<Kernel>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, RustyValue, Deserialize)]
|
||||
pub struct Kernel(pub String);
|
@ -1,8 +0,0 @@
|
||||
use crate::task;
|
||||
|
||||
task!(InstallTimeshift {
|
||||
name = "install-timeshift"
|
||||
args = TimeshiftConfig
|
||||
});
|
||||
|
||||
pub type TimeshiftConfig = ();
|
@ -1,8 +0,0 @@
|
||||
use crate::task;
|
||||
|
||||
task!(InstallZRamD {
|
||||
name = "install-zramd"
|
||||
args = ZRamDConfig
|
||||
});
|
||||
|
||||
pub type ZRamDConfig = ();
|
@ -1,118 +0,0 @@
|
||||
mod configure_locale;
|
||||
mod configure_network;
|
||||
mod configure_unakite;
|
||||
mod create_partitions;
|
||||
mod install_base;
|
||||
mod install_bootloader;
|
||||
mod install_desktop;
|
||||
mod install_extra_packages;
|
||||
mod install_flatpak;
|
||||
mod install_kernels;
|
||||
mod install_timeshift;
|
||||
mod install_zramd;
|
||||
mod setup_root_user;
|
||||
mod setup_users;
|
||||
|
||||
use std::{fmt, path::PathBuf};
|
||||
|
||||
pub use configure_locale::*;
|
||||
pub use configure_network::*;
|
||||
pub use configure_unakite::*;
|
||||
pub use create_partitions::*;
|
||||
pub use install_base::*;
|
||||
pub use install_bootloader::*;
|
||||
pub use install_desktop::*;
|
||||
pub use install_extra_packages::*;
|
||||
pub use install_flatpak::*;
|
||||
pub use install_kernels::*;
|
||||
pub use install_timeshift::*;
|
||||
pub use install_zramd::*;
|
||||
pub use setup_root_user::*;
|
||||
pub use setup_users::*;
|
||||
|
||||
use crate::scripting::script::{Script, ScriptArgs};
|
||||
|
||||
pub trait Task {
|
||||
type Config: ScriptArgs + fmt::Debug + Clone;
|
||||
type UpScript: Script<Args = Self::Config>;
|
||||
type DownScript: Script<Args = Self::Config>;
|
||||
|
||||
fn name() -> &'static str;
|
||||
}
|
||||
|
||||
/// Defines a script
|
||||
/// This macro doesn't accept a file extension for the script name
|
||||
/// as it is reused for the hook name
|
||||
#[macro_export]
|
||||
macro_rules! task {
|
||||
($task:ident {
|
||||
name = $name:literal
|
||||
args = $argtype:ident
|
||||
}) => {
|
||||
paste::item! {
|
||||
pub struct [<$task Task>];
|
||||
|
||||
impl $crate::tasks::Task for [<$task Task>] {
|
||||
type Config = $argtype;
|
||||
type UpScript = [<$task UpScript>];
|
||||
type DownScript = [<$task DownScript>];
|
||||
|
||||
#[inline]
|
||||
fn name() -> &'static str {
|
||||
$name
|
||||
}
|
||||
}
|
||||
|
||||
pub struct [<$task UpScript>];
|
||||
|
||||
impl $crate::scripting::script::Script for [<$task UpScript>] {
|
||||
type Args = $argtype;
|
||||
|
||||
#[inline]
|
||||
fn name() -> &'static str {
|
||||
concat!($name, "/up.nu")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct [<$task DownScript>];
|
||||
|
||||
impl $crate::scripting::script::Script for [<$task DownScript>] {
|
||||
type Args = $argtype;
|
||||
|
||||
#[inline]
|
||||
fn name() -> &'static str {
|
||||
concat!($name, "/down.nu")
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn all_tasks() -> Vec<(&'static str, PathBuf, PathBuf)> {
|
||||
macro_rules! task_scripts {
|
||||
($task:ident) => {{
|
||||
let base = PathBuf::from($task::name());
|
||||
(
|
||||
$task::name(),
|
||||
base.join(<$task as Task>::UpScript::name()),
|
||||
base.join(<$task as Task>::DownScript::name()),
|
||||
)
|
||||
}};
|
||||
}
|
||||
vec![
|
||||
task_scripts!(ConfigureLocaleTask),
|
||||
task_scripts!(ConfigureNetworkTask),
|
||||
task_scripts!(ConfigureUnakiteTask),
|
||||
task_scripts!(CreatePartitionsTask),
|
||||
task_scripts!(InstallBaseTask),
|
||||
task_scripts!(InstallBootloaderTask),
|
||||
task_scripts!(InstallDesktopTask),
|
||||
task_scripts!(InstallExtraPackagesTask),
|
||||
task_scripts!(InstallFlatpakTask),
|
||||
task_scripts!(InstallKernelsTask),
|
||||
task_scripts!(InstallTimeshiftTask),
|
||||
task_scripts!(InstallZRamDTask),
|
||||
task_scripts!(SetupRootUserTask),
|
||||
task_scripts!(SetupUsersTask),
|
||||
]
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
use embed_nu::rusty_value::*;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::task;
|
||||
|
||||
task!(SetupRootUser {
|
||||
name = "setup-root-user"
|
||||
args = RootUserConfig
|
||||
});
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, RustyValue)]
|
||||
pub struct RootUserConfig {
|
||||
pub password: String,
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
use embed_nu::rusty_value::*;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::task;
|
||||
|
||||
task!(SetupUsers {
|
||||
name = "setup-users"
|
||||
args = UsersConfig
|
||||
});
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, RustyValue)]
|
||||
pub struct UsersConfig {
|
||||
pub users: Vec<User>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, RustyValue)]
|
||||
pub struct User {
|
||||
pub name: String,
|
||||
pub password: String,
|
||||
pub sudoer: bool,
|
||||
pub shell: String,
|
||||
}
|
Loading…
Reference in New Issue