use super::error::KResult;
use super::settings::Settings;
use crate::types::Phase;
use ahash::AHashSet;
use camino::Utf8PathBuf;
use compact_str::CompactString;
use eyre::WrapErr;
use konfigkoll_types::FileContents;
use konfigkoll_types::FsInstruction;
use konfigkoll_types::FsOp;
use konfigkoll_types::FsOpDiscriminants;
use konfigkoll_types::PkgIdent;
use konfigkoll_types::PkgInstruction;
use konfigkoll_types::PkgInstructions;
use konfigkoll_types::PkgOp;
use konfigkoll_utils::safe_path_join;
use paketkoll_types::backend::Backend;
use paketkoll_types::files::Mode;
use rune::ContextError;
use rune::Module;
use rune::Value;
use std::str::FromStr;
use std::sync::Arc;
#[derive(Debug, Clone, rune::Any)]
#[rune(item = ::command)]
pub struct Commands {
pub(crate) phase: Phase,
pub(crate) base_files_path: Utf8PathBuf,
pub fs_ignores: AHashSet<CompactString>,
pub fs_actions: Vec<FsInstruction>,
pub package_actions: PkgInstructions,
settings: Arc<Settings>,
}
impl Commands {
pub(crate) fn new(base_files_path: Utf8PathBuf, settings: Arc<Settings>) -> Self {
Self {
phase: Phase::SystemDiscovery,
base_files_path,
fs_ignores: AHashSet::new(),
fs_actions: Vec::new(),
package_actions: PkgInstructions::new(),
settings,
}
}
pub(crate) fn file_contents(&self, path: &str) -> Option<&FileContents> {
self.fs_actions
.iter()
.rfind(|i| {
i.path == path && FsOpDiscriminants::from(&i.op) == FsOpDiscriminants::CreateFile
})
.map(|i| match &i.op {
FsOp::CreateFile(contents) => contents,
_ => unreachable!(),
})
}
fn verify_path(path: &str) -> eyre::Result<()> {
if path.contains("..") {
return Err(eyre::eyre!("Path {} contains '..'", path));
}
if !path.starts_with('/') {
return Err(eyre::eyre!("Path {} is not absolute", path));
}
Ok(())
}
}
impl Commands {
#[rune::function(keep)]
pub fn ignore_path(&mut self, ignore: &str) -> KResult<()> {
if self.phase != Phase::Ignores {
return Err(eyre::eyre!("Can only ignore paths during the 'ignores' phase").into());
}
if !self.fs_ignores.insert(ignore.into()) {
tracing::warn!("Ignoring path '{}' multiple times", ignore);
}
Ok(())
}
#[rune::function(keep)]
pub fn add_pkg(&mut self, package_manager: &str, identifier: &str) -> KResult<()> {
if self.phase < Phase::ScriptDependencies {
return Err(eyre::eyre!(
"Can only add packages during the 'script_dependencies' or 'main' phases"
)
.into());
}
let backend = Backend::from_str(package_manager).wrap_err("Invalid backend")?;
if !self.settings.is_pkg_backend_enabled(backend) {
tracing::debug!("Skipping disabled package manager {}", package_manager);
return Ok(());
}
if self
.package_actions
.insert(
PkgIdent {
package_manager: backend,
identifier: identifier.into(),
},
PkgInstruction {
op: PkgOp::Install,
comment: None,
},
)
.is_some()
{
tracing::warn!("Multiple actions for package '{package_manager}:{identifier}'",);
}
Ok(())
}
#[rune::function(keep)]
pub fn remove_pkg(&mut self, package_manager: &str, identifier: &str) -> KResult<()> {
if self.phase < Phase::ScriptDependencies {
return Err(eyre::eyre!(
"Can only add packages during the 'script_dependencies' or 'main' phases"
)
.into());
}
let backend = Backend::from_str(package_manager).wrap_err("Invalid backend")?;
if !self.settings.is_file_backend_enabled(backend) {
tracing::debug!("Skipping disabled package manager {}", package_manager);
return Ok(());
}
if self
.package_actions
.insert(
PkgIdent {
package_manager: backend,
identifier: identifier.into(),
},
PkgInstruction {
op: PkgOp::Uninstall,
comment: None,
},
)
.is_some()
{
tracing::warn!("Multiple actions for package '{package_manager}:{identifier}'",);
}
Ok(())
}
#[rune::function(keep)]
pub fn rm(&mut self, path: &str) -> KResult<()> {
if self.phase != Phase::Main {
return Err(
eyre::eyre!("File system actions are only possible in the 'main' phase").into(),
);
}
self.fs_actions.push(FsInstruction {
op: FsOp::Remove,
path: path.into(),
comment: None,
pkg: None,
});
Ok(())
}
#[rune::function(keep)]
#[must_use]
pub fn has_source_file(&self, path: &str) -> bool {
let path = safe_path_join(&self.base_files_path, path.into());
path.exists()
}
#[rune::function(keep)]
pub fn copy(&mut self, path: &str) -> KResult<()> {
self.copy_from(path, path)
}
#[rune::function(keep)]
pub fn copy_from(&mut self, path: &str, src: &str) -> KResult<()> {
if self.phase != Phase::Main {
return Err(
eyre::eyre!("File system actions are only possible in the 'main' phase").into(),
);
}
Self::verify_path(path)?;
Self::verify_path(src)?;
let contents = FileContents::from_file(&safe_path_join(&self.base_files_path, src.into()));
let contents = match contents {
Ok(v) => v,
Err(e) => {
tracing::error!("Failed to read file contents for '{}': {}", path, e);
return Err(
eyre::eyre!("Failed to read file contents for '{}': {}", path, e).into(),
);
}
};
self.fs_actions.push(FsInstruction {
op: FsOp::CreateFile(contents),
path: path.into(),
comment: None,
pkg: None,
});
Ok(())
}
#[rune::function(keep)]
pub fn ln(&mut self, path: &str, target: &str) -> KResult<()> {
if self.phase != Phase::Main {
return Err(
eyre::eyre!("File system actions are only possible in the 'main' phase").into(),
);
}
Self::verify_path(path)?;
self.fs_actions.push(FsInstruction {
op: FsOp::CreateSymlink {
target: target.into(),
},
path: path.into(),
comment: None,
pkg: None,
});
Ok(())
}
#[rune::function(keep)]
pub fn write(&mut self, path: &str, contents: &[u8]) -> KResult<()> {
if self.phase != Phase::Main {
return Err(
eyre::eyre!("File system actions are only possible in the 'main' phase").into(),
);
}
Self::verify_path(path)?;
self.fs_actions.push(FsInstruction {
op: FsOp::CreateFile(FileContents::from_literal(contents.into())),
path: path.into(),
comment: None,
pkg: None,
});
Ok(())
}
#[rune::function(keep)]
pub fn mkdir(&mut self, path: &str) -> KResult<()> {
if self.phase != Phase::Main {
return Err(
eyre::eyre!("File system actions are only possible in the 'main' phase").into(),
);
}
Self::verify_path(path)?;
self.fs_actions.push(FsInstruction {
op: FsOp::CreateDirectory,
path: path.into(),
comment: None,
pkg: None,
});
Ok(())
}
#[rune::function(keep)]
pub fn chown(&mut self, path: &str, owner: &str) -> KResult<()> {
if self.phase != Phase::Main {
return Err(
eyre::eyre!("File system actions are only possible in the 'main' phase").into(),
);
}
Self::verify_path(path)?;
self.fs_actions.push(FsInstruction {
op: FsOp::SetOwner {
owner: owner.into(),
},
path: path.into(),
comment: None,
pkg: None,
});
Ok(())
}
#[rune::function(keep)]
pub fn chgrp(&mut self, path: &str, group: &str) -> KResult<()> {
if self.phase != Phase::Main {
return Err(
eyre::eyre!("File system actions are only possible in the 'main' phase").into(),
);
}
Self::verify_path(path)?;
self.fs_actions.push(FsInstruction {
op: FsOp::SetGroup {
group: group.into(),
},
path: path.into(),
comment: None,
pkg: None,
});
Ok(())
}
#[rune::function(keep)]
pub fn chmod(&mut self, path: &str, mode: Value) -> KResult<()> {
if self.phase != Phase::Main {
return Err(
eyre::eyre!("File system actions are only possible in the 'main' phase").into(),
);
}
Self::verify_path(path)?;
let numeric_mode = match mode {
Value::Integer(m) => Mode::new(m as u32),
Value::String(str) => {
let guard = str.borrow_ref().wrap_err("Borrow guard failed")?;
Mode::parse(&guard)?
}
_ => return Err(eyre::eyre!("Invalid mode value").into()),
};
self.fs_actions.push(FsInstruction {
op: FsOp::SetMode { mode: numeric_mode },
path: path.into(),
comment: None,
pkg: None,
});
Ok(())
}
#[rune::function(keep)]
pub fn perms(&mut self, path: &str, owner: &str, group: &str, mode: Value) -> KResult<()> {
self.chown(path, owner)?;
self.chgrp(path, group)?;
self.chmod(path, mode)?;
Ok(())
}
}
#[rune::module(::command)]
pub(crate) fn module() -> Result<Module, ContextError> {
let mut m = Module::from_meta(module_meta)?;
m.ty::<Commands>()?;
m.function_meta(Commands::ignore_path__meta)?;
m.function_meta(Commands::add_pkg__meta)?;
m.function_meta(Commands::remove_pkg__meta)?;
m.function_meta(Commands::rm__meta)?;
m.function_meta(Commands::has_source_file__meta)?;
m.function_meta(Commands::copy__meta)?;
m.function_meta(Commands::copy_from__meta)?;
m.function_meta(Commands::ln__meta)?;
m.function_meta(Commands::write__meta)?;
m.function_meta(Commands::mkdir__meta)?;
m.function_meta(Commands::chown__meta)?;
m.function_meta(Commands::chgrp__meta)?;
m.function_meta(Commands::chmod__meta)?;
m.function_meta(Commands::perms__meta)?;
Ok(m)
}