use crate::{
git::GitSystem, hierarchy::Data, os, packages, packages::PackageManager, state::State,
FileSystem, Timestamp,
};
use anyhow::{anyhow, Context as _, Error};
use std::collections::BTreeSet;
use std::fmt;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::SystemTime;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Dependency {
File(UnitId),
Dir(UnitId),
Unit(UnitId),
}
#[derive(Error, Debug)]
pub struct RenderError(PathBuf);
impl fmt::Display for RenderError {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(fmt, "failed to render: {}", self.0.display())
}
}
pub type UnitId = usize;
#[derive(Debug, Default)]
pub struct UnitAllocator {
current: AtomicUsize,
}
impl UnitAllocator {
pub fn unit(&self, unit: impl Into<Unit>) -> SystemUnit {
let id = self.allocate();
SystemUnit::new(id, unit)
}
pub fn allocate(&self) -> UnitId {
self.current.fetch_add(1, Ordering::Relaxed)
}
}
pub struct UnitInput<'a, 's> {
pub packages: &'a packages::Provider,
pub data: &'a Data,
pub read_state: &'a State<'s>,
pub state: &'a mut State<'s>,
pub now: Timestamp,
pub git_system: &'a dyn GitSystem,
}
macro_rules! unit {
($($name:ident,)*) => {
#[derive(Debug)]
pub enum Unit {
System,
$($name($name),)*
}
impl Unit {
pub fn apply(&self, input: UnitInput) -> Result<(), Error> {
use self::Unit::*;
let res = match *self {
System => Ok(()),
$($name(ref unit) => unit.apply(input),)*
};
res.with_context(|| anyhow!("Failed to run unit: {:?}", self))
}
}
impl fmt::Display for Unit {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
use self::Unit::*;
match *self {
System => write!(fmt, "system unit"),
$($name(ref unit) => unit.fmt(fmt),)*
}
}
}
}
}
unit![
FromDb,
CopyFile,
CopyTemplate,
Symlink,
CreateDir,
Install,
Download,
AddMode,
RunOnce,
GitClone,
GitUpdate,
];
#[derive(Debug)]
pub struct SystemUnit {
pub id: UnitId,
pub dependencies: Vec<Dependency>,
pub provides: Vec<Dependency>,
pub thread_local: bool,
unit: Box<Unit>,
}
impl fmt::Display for SystemUnit {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(
fmt,
"unit({:03}): {} (depends: {:?})",
self.id, self.unit, self.dependencies
)
}
}
impl SystemUnit {
pub fn new(id: UnitId, unit: impl Into<Unit>) -> Self {
SystemUnit {
id,
dependencies: Vec::new(),
provides: Vec::new(),
thread_local: false,
unit: Box::new(unit.into()),
}
}
pub fn apply(&self, input: UnitInput) -> Result<(), Error> {
self.unit.apply(input)
}
}
#[derive(Debug, Hash)]
pub struct FromDb {
pub(crate) system: String,
pub(crate) key: String,
}
impl fmt::Display for FromDb {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(
fmt,
"unit `{}` from database key `{}`",
self.system, self.key
)
}
}
impl FromDb {
fn apply(&self, _: UnitInput) -> Result<(), Error> {
Ok(())
}
}
impl From<FromDb> for Unit {
fn from(value: FromDb) -> Unit {
Unit::FromDb(value)
}
}
#[derive(Debug)]
pub struct CreateDir(pub PathBuf);
impl fmt::Display for CreateDir {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(fmt, "create directory {}", self.0.display())
}
}
impl CreateDir {
fn apply(&self, _: UnitInput) -> Result<(), Error> {
use std::fs;
let CreateDir(ref dir) = self;
log::info!("creating dir: {}", dir.display());
fs::create_dir(dir)?;
Ok(())
}
}
impl From<CreateDir> for Unit {
fn from(value: CreateDir) -> Unit {
Unit::CreateDir(value)
}
}
#[derive(Debug, Hash)]
pub struct CopyFile {
pub from: PathBuf,
pub from_modified: SystemTime,
pub to: PathBuf,
}
impl fmt::Display for CopyFile {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(
fmt,
"copy file {} -> {}",
self.from.display(),
self.to.display()
)
}
}
impl CopyFile {
fn apply(&self, _: UnitInput) -> Result<(), Error> {
use std::fs::File;
use std::io;
let CopyFile {
ref from,
ref from_modified,
ref to,
} = *self;
log::info!("{} -> {}", from.display(), to.display());
io::copy(&mut File::open(from)?, &mut File::create(to)?)?;
FileSystem::touch(to, from_modified)
}
}
impl From<CopyFile> for Unit {
fn from(value: CopyFile) -> Unit {
Unit::CopyFile(value)
}
}
#[derive(Debug, Hash)]
pub struct CopyTemplate {
pub from: PathBuf,
pub from_modified: SystemTime,
pub to: PathBuf,
pub to_exists: bool,
}
impl fmt::Display for CopyTemplate {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(
fmt,
"template file {} -> {}",
self.from.display(),
self.to.display()
)
}
}
impl CopyTemplate {
fn id(&self) -> String {
use std::hash::{Hash, Hasher};
let mut state = fxhash::FxHasher64::default();
self.hash(&mut state);
format!("copy-template/{:x}", state.finish())
}
fn apply(&self, input: UnitInput) -> Result<(), Error> {
use handlebars::{Context, Handlebars, Output, RenderContext, Renderable, Template};
use std::fs::{self, File};
use std::io::{self, Cursor, Write};
let CopyTemplate {
ref from,
ref from_modified,
ref to,
to_exists,
} = *self;
let UnitInput {
data,
read_state,
state,
..
} = input;
let content = fs::read_to_string(from)
.map_err(|e| anyhow!("failed to read path: {}: {}", from.display(), e))?;
let data = data.load_from_spec(&content).map_err(|e| {
anyhow!(
"failed to load hierarchy for path: {}: {}",
from.display(),
e
)
})?;
let id = self.id();
let hash = (&data, &content);
if to_exists && read_state.is_hash_fresh(&id, hash)? {
log::info!("touching {}", to.display());
return FileSystem::touch(to, from_modified);
}
let reg = Handlebars::new();
let mut out = Vec::<u8>::new();
let mut tpl = Template::compile(&content)?;
tpl.name = Some(from.display().to_string());
tpl.render(
®,
&Context::wraps(&data)?,
&mut RenderContext::new(None),
&mut WriteOutput::new(Cursor::new(&mut out)),
)?;
log::info!("{} -> {} (template)", from.display(), to.display());
File::create(to)?.write_all(&out)?;
state.touch_hash(&id, hash)?;
return FileSystem::touch(to, from_modified);
pub struct WriteOutput<W: Write> {
write: W,
}
impl<W: Write> Output for WriteOutput<W> {
fn write(&mut self, seg: &str) -> Result<(), io::Error> {
self.write.write_all(seg.as_bytes())
}
}
impl<W: Write> WriteOutput<W> {
pub fn new(write: W) -> WriteOutput<W> {
WriteOutput { write }
}
}
}
}
impl From<CopyTemplate> for Unit {
fn from(value: CopyTemplate) -> Unit {
Unit::CopyTemplate(value)
}
}
#[derive(Debug)]
pub struct Symlink {
pub remove: bool,
pub path: PathBuf,
pub link: PathBuf,
}
impl fmt::Display for Symlink {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(
fmt,
"link file {} to {}",
self.path.display(),
self.link.display()
)
}
}
impl Symlink {
fn apply(&self, _: UnitInput) -> Result<(), Error> {
os::create_symlink(self)
}
}
impl From<Symlink> for Unit {
fn from(value: Symlink) -> Unit {
Unit::Symlink(value)
}
}
#[derive(Debug)]
pub struct Install {
pub package_manager: Arc<dyn PackageManager>,
pub all_packages: BTreeSet<String>,
pub to_install: Vec<String>,
pub id: String,
}
impl fmt::Display for Install {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
if self.to_install.is_empty() {
return write!(fmt, "install packages");
}
let names = self.to_install.join(", ");
write!(fmt, "{}: install packages: {}", self.id, names)
}
}
impl Install {
fn apply(&self, input: UnitInput) -> Result<(), Error> {
let UnitInput { state, .. } = input;
let Install {
ref package_manager,
ref all_packages,
ref to_install,
ref id,
} = *self;
if !to_install.is_empty() {
let names = to_install.join(", ");
log::info!("Installing packages for `{}`: {}", id, names);
package_manager.install_packages(to_install)?;
}
state.touch_hash(id, all_packages)?;
Ok(())
}
}
impl From<Install> for Unit {
fn from(value: Install) -> Unit {
Unit::Install(value)
}
}
#[derive(Debug)]
pub struct Download {
pub url: reqwest::Url,
pub path: PathBuf,
pub id: Option<Box<str>>,
}
impl fmt::Display for Download {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(fmt, "download {} to {}", self.url, self.path.display())
}
}
impl Download {
fn apply(&self, input: UnitInput) -> Result<(), Error> {
use std::fs::File;
let UnitInput { state, .. } = input;
let Download { url, path, id } = self;
if !path.is_file() {
let mut out =
File::create(path).with_context(|| anyhow!("open file: {}", path.display()))?;
let mut response = reqwest::blocking::get(url.clone())
.with_context(|| anyhow!("download url: {}", url))?;
response.copy_to(&mut out)?;
}
if let Some(id) = id {
state.touch_once(id);
}
Ok(())
}
}
impl From<Download> for Unit {
fn from(value: Download) -> Unit {
Unit::Download(value)
}
}
#[repr(u32)]
pub enum Mode {
Execute = 1,
Read = 2,
Write = 4,
}
#[derive(Debug)]
pub struct AddMode {
pub path: PathBuf,
user: u32,
group: u32,
other: u32,
}
impl AddMode {
pub fn new<P>(path: &P) -> Self
where
P: ?Sized + AsRef<Path>,
{
Self {
path: path.as_ref().to_owned(),
user: 0,
group: 0,
other: 0,
}
}
pub fn is_executable(&self) -> bool {
if self.user & (Mode::Execute as u32) != 0 {
return true;
}
if self.group & (Mode::Execute as u32) != 0 {
return true;
}
if self.other & (Mode::Execute as u32) != 0 {
return true;
}
false
}
pub fn user(mut self, mode: Mode) -> Self {
self.user |= mode as u32;
self
}
pub fn group(mut self, mode: Mode) -> Self {
self.group |= mode as u32;
self
}
pub fn other(mut self, mode: Mode) -> Self {
self.other |= mode as u32;
self
}
}
impl AddMode {
pub fn unix_mode(&self) -> u32 {
let AddMode {
user, group, other, ..
} = *self;
(user << (3 * 2)) + (group << 3) + other
}
}
impl fmt::Display for AddMode {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(
fmt,
"add mode {:o} to {}",
self.unix_mode(),
self.path.display()
)
}
}
impl AddMode {
fn apply(&self, _: UnitInput) -> Result<(), Error> {
os::add_mode(self)
}
}
impl From<AddMode> for Unit {
fn from(value: AddMode) -> Unit {
Unit::AddMode(value)
}
}
#[derive(Debug)]
pub struct RunOnce {
pub id: String,
pub path: PathBuf,
pub shell: bool,
pub root: bool,
pub args: Vec<String>,
}
impl fmt::Display for RunOnce {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(fmt, "run `{}` once as `{}`", self.path.display(), self.id)
}
}
impl RunOnce {
pub fn new(id: String, path: PathBuf) -> RunOnce {
RunOnce {
id,
path,
shell: false,
root: false,
args: Vec::new(),
}
}
fn apply(&self, input: UnitInput) -> Result<(), Error> {
use crate::command::Command;
use std::io;
let UnitInput { state, .. } = input;
let RunOnce {
ref id,
ref path,
shell,
root,
ref args,
} = *self;
if self.args.is_empty() {
log::info!("running: {}", path.display());
} else {
log::info!("running: {} {}", path.display(), self.args.join(" "));
}
let status = run_command(path, root, shell, args)
.with_context(|| anyhow!("failed to run: {}", path.display()))?;
if status != 0 {
return Err(anyhow!(
"failed to run `{}`: status={}",
path.display(),
status
));
}
state.touch_once(id);
return Ok(());
#[cfg(windows)]
fn run_command(
path: &Path,
root: bool,
_shell: bool,
args: &Vec<String>,
) -> io::Result<i32> {
let mut cmd = Command::new(path);
cmd.args(args);
Ok(if root {
cmd.runas()?
} else {
let status = cmd.status()?;
status
.code()
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "no status code"))?
})
}
#[cfg(not(windows))]
fn run_command(
path: &Path,
root: bool,
shell: bool,
args: &Vec<String>,
) -> io::Result<i32> {
let mut cmd = if root {
let mut cmd = Command::new("sudo");
cmd.args(&["-p", "[sudo] password for %u to run downloaded exe: ", "--"]);
if shell {
cmd.arg("/bin/sh");
cmd.arg("--");
cmd.arg(path);
} else {
cmd.arg(path);
}
cmd
} else if shell {
let mut cmd = Command::new("/bin/sh");
cmd.arg(path);
cmd
} else {
Command::new(path)
};
cmd.args(args);
let status = cmd.status()?;
let code = status
.code()
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "no status code"))?;
Ok(code)
}
}
}
impl From<RunOnce> for Unit {
fn from(value: RunOnce) -> Unit {
Unit::RunOnce(value)
}
}
#[derive(Debug)]
pub struct GitClone {
pub id: String,
pub remote: String,
pub path: PathBuf,
}
impl fmt::Display for GitClone {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(
fmt,
"git clone `{}` to `{}`",
self.remote,
self.path.display()
)
}
}
impl GitClone {
fn apply(&self, input: UnitInput) -> Result<(), Error> {
let UnitInput {
state, git_system, ..
} = input;
let GitClone {
ref id,
ref remote,
ref path,
} = *self;
log::info!("Cloning `{}` into `{}`", remote, path.display());
GitSystem::clone(git_system, remote, path)?;
state.touch(id);
Ok(())
}
}
impl From<GitClone> for Unit {
fn from(value: GitClone) -> Unit {
Unit::GitClone(value)
}
}
#[derive(Debug)]
pub struct GitUpdate {
pub id: String,
pub path: PathBuf,
pub force: bool,
}
impl fmt::Display for GitUpdate {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(fmt, "git update: {}", self.path.display())
}
}
impl GitUpdate {
fn apply(&self, input: UnitInput) -> Result<(), Error> {
let UnitInput {
state, git_system, ..
} = input;
let GitUpdate {
ref id,
ref path,
force,
} = *self;
let git = git_system.open(path)?;
if git.needs_update()? {
if force {
log::info!("Force updating `{}`", git.path().display());
git.force_update()?;
} else {
log::info!("Updating `{}`", git.path().display());
git.update()?;
}
}
state.touch(id);
Ok(())
}
}
impl From<GitUpdate> for Unit {
fn from(value: GitUpdate) -> Unit {
Unit::GitUpdate(value)
}
}