More bones, trimming deps
This commit is contained in:
parent
609ed3563f
commit
fe966932ed
5 changed files with 244 additions and 58 deletions
|
@ -6,7 +6,6 @@ edition = "2021"
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-compression = { version = "0.3.14", features = ["xz", "futures-io"] }
|
|
||||||
async-tar = "0.4.2"
|
async-tar = "0.4.2"
|
||||||
async-trait = "0.1.57"
|
async-trait = "0.1.57"
|
||||||
atty = "0.2.14"
|
atty = "0.2.14"
|
||||||
|
@ -16,11 +15,17 @@ crossterm = { version = "0.25.0", features = ["event-stream"] }
|
||||||
eyre = "0.6.8"
|
eyre = "0.6.8"
|
||||||
futures = "0.3.24"
|
futures = "0.3.24"
|
||||||
owo-colors = { version = "3.5.0", features = [ "supports-colors" ] }
|
owo-colors = { version = "3.5.0", features = [ "supports-colors" ] }
|
||||||
reqwest = { version = "0.11.11", features = ["native-tls", "stream"] }
|
reqwest = { version = "0.11.11", default-features = false, features = ["rustls-tls", "stream"] }
|
||||||
target-lexicon = "0.12.4"
|
target-lexicon = "0.12.4"
|
||||||
thiserror = "1.0.33"
|
thiserror = "1.0.33"
|
||||||
tokio = { version = "1.21.0", features = ["time", "io-std", "process", "fs", "tracing", "rt-multi-thread", "macros", "io-util"] }
|
tokio = { version = "1.21.0", features = ["time", "io-std", "process", "fs", "tracing", "rt-multi-thread", "macros", "io-util"] }
|
||||||
|
tokio-util = { version = "0.7", features = ["io"] }
|
||||||
tracing = { version = "0.1.36", features = [ "valuable" ] }
|
tracing = { version = "0.1.36", features = [ "valuable" ] }
|
||||||
tracing-error = "0.2.0"
|
tracing-error = "0.2.0"
|
||||||
tracing-subscriber = { version = "0.3.15", features = [ "env-filter", "valuable" ] }
|
tracing-subscriber = { version = "0.3.15", features = [ "env-filter", "valuable" ] }
|
||||||
valuable = { version = "0.1.0", features = ["derive"] }
|
valuable = { version = "0.1.0", features = ["derive"] }
|
||||||
|
tempdir = { version = "0.3.7"}
|
||||||
|
glob = "0.3.0"
|
||||||
|
xz2 = { version = "0.1.7", features = ["static", "tokio"] }
|
||||||
|
bytes = "1.2.1"
|
||||||
|
tar = "0.4.38"
|
||||||
|
|
|
@ -64,6 +64,8 @@ impl CommandExecute for HarmonicCli {
|
||||||
harmonic.place_channel_configuration().await?;
|
harmonic.place_channel_configuration().await?;
|
||||||
harmonic.fetch_nix().await?;
|
harmonic.fetch_nix().await?;
|
||||||
harmonic.configure_shell_profile().await?;
|
harmonic.configure_shell_profile().await?;
|
||||||
|
harmonic.setup_default_profile().await?;
|
||||||
|
harmonic.place_nix_configuration().await?;
|
||||||
|
|
||||||
Ok(ExitCode::SUCCESS)
|
Ok(ExitCode::SUCCESS)
|
||||||
}
|
}
|
||||||
|
|
31
src/error.rs
31
src/error.rs
|
@ -16,4 +16,35 @@ pub enum HarmonicError {
|
||||||
CreateDirectory(std::io::Error),
|
CreateDirectory(std::io::Error),
|
||||||
#[error("Placing channel configuration")]
|
#[error("Placing channel configuration")]
|
||||||
PlaceChannelConfiguration(std::io::Error),
|
PlaceChannelConfiguration(std::io::Error),
|
||||||
|
#[error("Opening file `{0}`")]
|
||||||
|
OpeningFile(std::path::PathBuf, std::io::Error),
|
||||||
|
#[error("Writing to file `{0}`")]
|
||||||
|
WritingFile(std::path::PathBuf, std::io::Error),
|
||||||
|
#[error("Getting tempdir")]
|
||||||
|
GettingTempDir(std::io::Error),
|
||||||
|
#[error("Installing fetched Nix into the new store")]
|
||||||
|
InstallNixIntoStore(std::io::Error),
|
||||||
|
#[error("Installing fetched nss-cacert into the new store")]
|
||||||
|
InstallNssCacertIntoStore(std::io::Error),
|
||||||
|
#[error("Updating the Nix channel")]
|
||||||
|
UpdatingNixChannel(std::io::Error),
|
||||||
|
#[error("Globbing pattern error")]
|
||||||
|
GlobPatternError(glob::PatternError),
|
||||||
|
#[error("Could not find nss-cacert")]
|
||||||
|
NoNssCacert,
|
||||||
|
#[error("Creating /etc/nix/nix.conf")]
|
||||||
|
CreatingNixConf(std::io::Error),
|
||||||
|
#[error("No supported init syustem found")]
|
||||||
|
InitNotSupported,
|
||||||
|
#[error("Linking `{0}` to `{1}`")]
|
||||||
|
Linking(std::path::PathBuf, std::path::PathBuf, std::io::Error),
|
||||||
|
#[error("Running `systemd-tmpfiles`")]
|
||||||
|
SystemdTmpfiles(std::io::Error),
|
||||||
|
#[error("Command `{0}` failed to execute")]
|
||||||
|
CommandFailedExec(String, std::io::Error),
|
||||||
|
// TODO(@Hoverbear): This should capture the stdout.
|
||||||
|
#[error("Command `{0}` did not to return a success status")]
|
||||||
|
CommandFailedStatus(String),
|
||||||
|
#[error("Join error")]
|
||||||
|
JoinError(#[from] tokio::task::JoinError),
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,26 +2,7 @@ use crossterm::event::{EventStream, KeyCode};
|
||||||
use eyre::{eyre, WrapErr};
|
use eyre::{eyre, WrapErr};
|
||||||
use futures::{FutureExt, StreamExt};
|
use futures::{FutureExt, StreamExt};
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::OwoColorize;
|
||||||
use tokio::{io::AsyncWriteExt, process::Command};
|
use tokio::io::AsyncWriteExt;
|
||||||
|
|
||||||
pub(crate) async fn confirm_command(
|
|
||||||
question: impl AsRef<str>,
|
|
||||||
command: Command,
|
|
||||||
) -> eyre::Result<bool> {
|
|
||||||
confirm(format!(
|
|
||||||
"\
|
|
||||||
{question}\n\
|
|
||||||
\n\
|
|
||||||
{ticks}\n\
|
|
||||||
{command_styled}\n\
|
|
||||||
{ticks}\n\
|
|
||||||
",
|
|
||||||
question = question.as_ref(),
|
|
||||||
ticks = "```".dimmed(),
|
|
||||||
command_styled = format!("{:?}", command.as_std()).green(),
|
|
||||||
))
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn confirm(question: impl AsRef<str>) -> eyre::Result<bool> {
|
pub(crate) async fn confirm(question: impl AsRef<str>) -> eyre::Result<bool> {
|
||||||
let mut stdout = tokio::io::stdout();
|
let mut stdout = tokio::io::stdout();
|
||||||
|
@ -43,8 +24,9 @@ pub(crate) async fn confirm(question: impl AsRef<str>) -> eyre::Result<bool> {
|
||||||
loop {
|
loop {
|
||||||
let event = reader.next().fuse().await;
|
let event = reader.next().fuse().await;
|
||||||
match event {
|
match event {
|
||||||
Some(Ok(event)) => match event {
|
Some(Ok(event)) => {
|
||||||
crossterm::event::Event::Key(key) => match key.code {
|
if let crossterm::event::Event::Key(key) = event {
|
||||||
|
match key.code {
|
||||||
KeyCode::Char('y') => break Ok(true),
|
KeyCode::Char('y') => break Ok(true),
|
||||||
_ => {
|
_ => {
|
||||||
stdout
|
stdout
|
||||||
|
@ -53,9 +35,9 @@ pub(crate) async fn confirm(question: impl AsRef<str>) -> eyre::Result<bool> {
|
||||||
stdout.flush().await?;
|
stdout.flush().await?;
|
||||||
break Ok(false);
|
break Ok(false);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
_ => (),
|
}
|
||||||
},
|
}
|
||||||
Some(Err(err)) => return Err(err).wrap_err("Getting response"),
|
Some(Err(err)) => return Err(err).wrap_err("Getting response"),
|
||||||
None => return Err(eyre!("Bailed, no confirmation event")),
|
None => return Err(eyre!("Bailed, no confirmation event")),
|
||||||
}
|
}
|
||||||
|
@ -66,8 +48,3 @@ pub(crate) async fn clean_exit_with_message(message: impl AsRef<str>) -> ! {
|
||||||
eprintln!("{}", message.as_ref());
|
eprintln!("{}", message.as_ref());
|
||||||
std::process::exit(0)
|
std::process::exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn angry_bail_with_message(message: impl AsRef<str>, code: i32) -> ! {
|
|
||||||
eprintln!("{}", message.as_ref());
|
|
||||||
std::process::exit(code)
|
|
||||||
}
|
|
||||||
|
|
201
src/lib.rs
201
src/lib.rs
|
@ -1,5 +1,10 @@
|
||||||
mod error;
|
mod error;
|
||||||
use std::{fs::Permissions, os::unix::fs::PermissionsExt, path::Path};
|
use std::{
|
||||||
|
fs::Permissions,
|
||||||
|
os::unix::fs::PermissionsExt,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
process::ExitStatus,
|
||||||
|
};
|
||||||
|
|
||||||
pub use error::HarmonicError;
|
pub use error::HarmonicError;
|
||||||
|
|
||||||
|
@ -8,12 +13,14 @@ mod nixos;
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub use nixos::NixOs;
|
pub use nixos::NixOs;
|
||||||
|
|
||||||
use futures::stream::TryStreamExt;
|
use bytes::Buf;
|
||||||
|
use glob::glob;
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
fs::{create_dir, set_permissions, OpenOptions},
|
fs::{create_dir, create_dir_all, set_permissions, symlink, OpenOptions},
|
||||||
io::AsyncWriteExt,
|
io::AsyncWriteExt,
|
||||||
process::Command,
|
process::Command,
|
||||||
|
task::spawn_blocking,
|
||||||
};
|
};
|
||||||
|
|
||||||
// This uses a Rust builder pattern
|
// This uses a Rust builder pattern
|
||||||
|
@ -54,21 +61,22 @@ impl Harmonic {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(HarmonicError::DownloadingNix)?;
|
.map_err(HarmonicError::DownloadingNix)?;
|
||||||
let stream = res.bytes_stream();
|
let bytes = res.bytes().await.map_err(HarmonicError::DownloadingNix)?;
|
||||||
let async_read = stream
|
|
||||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
|
|
||||||
.into_async_read();
|
|
||||||
let buffered = futures::io::BufReader::new(async_read);
|
|
||||||
let decoder = async_compression::futures::bufread::XzDecoder::new(buffered);
|
|
||||||
let archive = async_tar::Archive::new(decoder);
|
|
||||||
|
|
||||||
// TODO(@Hoverbear): Pick directory
|
// TODO(@Hoverbear): Pick directory
|
||||||
let destination = "/nix/store";
|
let handle: Result<(), HarmonicError> = spawn_blocking(|| {
|
||||||
|
let decoder = xz2::read::XzDecoder::new(bytes.reader());
|
||||||
|
let mut archive = tar::Archive::new(decoder);
|
||||||
|
let destination = "/nix/install";
|
||||||
archive
|
archive
|
||||||
.unpack(destination)
|
.unpack(destination)
|
||||||
.await
|
|
||||||
.map_err(HarmonicError::UnpackingNix)?;
|
.map_err(HarmonicError::UnpackingNix)?;
|
||||||
tracing::debug!(%destination, "Downloaded & extracted Nix");
|
tracing::debug!(%destination, "Downloaded & extracted Nix");
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
handle?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,7 +130,7 @@ impl Harmonic {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
pub async fn create_directories(&self) -> Result<(), HarmonicError> {
|
pub async fn create_directories(&self) -> Result<(), HarmonicError> {
|
||||||
let permissions = Permissions::from_mode(755);
|
let permissions = Permissions::from_mode(0o755);
|
||||||
let paths = [
|
let paths = [
|
||||||
"/nix",
|
"/nix",
|
||||||
"/nix/var",
|
"/nix/var",
|
||||||
|
@ -168,7 +176,158 @@ impl Harmonic {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
pub async fn configure_shell_profile(&self) -> Result<(), HarmonicError> {
|
pub async fn configure_shell_profile(&self) -> Result<(), HarmonicError> {
|
||||||
todo!();
|
const PROFILE_TARGETS: &[&str] = &[
|
||||||
|
"/etc/bashrc",
|
||||||
|
"/etc/profile.d/nix.sh",
|
||||||
|
"/etc/zshrc",
|
||||||
|
"/etc/bash.bashrc",
|
||||||
|
"/etc/zsh/zshrc",
|
||||||
|
];
|
||||||
|
const PROFILE_NIX_FILE: &str = "/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh";
|
||||||
|
for profile_target in PROFILE_TARGETS {
|
||||||
|
let path = Path::new(profile_target);
|
||||||
|
let buf = format!(
|
||||||
|
"\n\
|
||||||
|
# Nix\n\
|
||||||
|
if [ -e '{PROFILE_NIX_FILE}' ]; then\n\
|
||||||
|
. '{PROFILE_NIX_FILE}'\n\
|
||||||
|
fi\n
|
||||||
|
# End Nix\n
|
||||||
|
\n",
|
||||||
|
);
|
||||||
|
if path.exists() {
|
||||||
|
// TODO(@Hoverbear): Backup
|
||||||
|
// TODO(@Hoverbear): See if the line already exists, skip setting it
|
||||||
|
tracing::trace!("TODO");
|
||||||
|
} else if let Some(parent) = path.parent() {
|
||||||
|
create_dir_all(parent).await.unwrap()
|
||||||
|
}
|
||||||
|
let mut file = OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.truncate(false)
|
||||||
|
.open(profile_target)
|
||||||
|
.await
|
||||||
|
.map_err(|e| HarmonicError::OpeningFile(path.to_owned(), e))?;
|
||||||
|
file.write_all(buf.as_bytes())
|
||||||
|
.await
|
||||||
|
.map_err(|e| HarmonicError::WritingFile(path.to_owned(), e))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn setup_default_profile(&self) -> Result<(), HarmonicError> {
|
||||||
|
Command::new("/nix/install/bin/nix-env")
|
||||||
|
.arg("-i")
|
||||||
|
.arg("/nix/install")
|
||||||
|
.status()
|
||||||
|
.await
|
||||||
|
.map_err(HarmonicError::InstallNixIntoStore)?;
|
||||||
|
// Find an `nss-cacert` package, add it too.
|
||||||
|
let mut found_nss_ca_cert = None;
|
||||||
|
for entry in
|
||||||
|
glob("/nix/install/store/*-nss-cacert").map_err(HarmonicError::GlobPatternError)?
|
||||||
|
{
|
||||||
|
match entry {
|
||||||
|
Ok(path) => {
|
||||||
|
// TODO(@Hoverbear): Should probably ensure is unique
|
||||||
|
found_nss_ca_cert = Some(path);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(_) => continue, /* Ignore it */
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if let Some(nss_ca_cert) = found_nss_ca_cert {
|
||||||
|
let status = Command::new("/nix/install/bin/nix-env")
|
||||||
|
.arg("-i")
|
||||||
|
.arg(&nss_ca_cert)
|
||||||
|
.status()
|
||||||
|
.await
|
||||||
|
.map_err(HarmonicError::InstallNssCacertIntoStore)?;
|
||||||
|
if !status.success() {
|
||||||
|
// TODO(@Hoverbear): report
|
||||||
|
}
|
||||||
|
std::env::set_var("NIX_SSL_CERT_FILE", &nss_ca_cert);
|
||||||
|
} else {
|
||||||
|
return Err(HarmonicError::NoNssCacert);
|
||||||
|
}
|
||||||
|
if !self.channels.is_empty() {
|
||||||
|
status_failure_as_error(
|
||||||
|
Command::new("/nix/install/bin/nix-channel")
|
||||||
|
.arg("--update")
|
||||||
|
.arg("nixpkgs"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn place_nix_configuration(&self) -> Result<(), HarmonicError> {
|
||||||
|
let mut nix_conf = OpenOptions::new()
|
||||||
|
.create_new(true)
|
||||||
|
.write(true)
|
||||||
|
.read(true)
|
||||||
|
.open("/etc/nix/nix.conf")
|
||||||
|
.await
|
||||||
|
.map_err(HarmonicError::CreatingNixConf)?;
|
||||||
|
let buf = format!(
|
||||||
|
"\
|
||||||
|
{extra_conf}\n\
|
||||||
|
build-users-group = {build_group_name}\n\
|
||||||
|
",
|
||||||
|
extra_conf = "", // TODO(@Hoverbear): populate me
|
||||||
|
build_group_name = self.nix_build_group_name,
|
||||||
|
);
|
||||||
|
nix_conf
|
||||||
|
.write_all(buf.as_bytes())
|
||||||
|
.await
|
||||||
|
.map_err(HarmonicError::CreatingNixConf)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn configure_nix_daemon_service(&self) -> Result<(), HarmonicError> {
|
||||||
|
if Path::new("/run/systemd/system").exists() {
|
||||||
|
const SERVICE_SRC: &str =
|
||||||
|
"/nix/var/nix/profiles/default/lib/systemd/system/nix-daemon.service";
|
||||||
|
|
||||||
|
const SOCKET_SRC: &str =
|
||||||
|
"/nix/var/nix/profiles/default/lib/systemd/system/nix-daemon.socket";
|
||||||
|
|
||||||
|
const TMPFILES_SRC: &str =
|
||||||
|
"/nix/var/nix/profiles/default//lib/tmpfiles.d/nix-daemon.conf";
|
||||||
|
const TMPFILES_DEST: &str = "/etc/tmpfiles.d/nix-daemon.conf";
|
||||||
|
|
||||||
|
symlink(TMPFILES_SRC, TMPFILES_DEST).await.map_err(|e| {
|
||||||
|
HarmonicError::Linking(PathBuf::from(TMPFILES_SRC), PathBuf::from(TMPFILES_DEST), e)
|
||||||
|
})?;
|
||||||
|
status_failure_as_error(
|
||||||
|
Command::new("systemd-tmpfiles")
|
||||||
|
.arg("--create")
|
||||||
|
.arg("--prefix=/nix/var/nix"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
status_failure_as_error(Command::new("systemctl").arg("link").arg(SERVICE_SRC)).await?;
|
||||||
|
status_failure_as_error(Command::new("systemctl").arg("enable").arg(SOCKET_SRC))
|
||||||
|
.await?;
|
||||||
|
// TODO(@Hoverbear): Handle proxy vars
|
||||||
|
status_failure_as_error(Command::new("systemctl").arg("daemon-reload")).await?;
|
||||||
|
status_failure_as_error(
|
||||||
|
Command::new("systemctl")
|
||||||
|
.arg("start")
|
||||||
|
.arg("nix-daemon.socket"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
status_failure_as_error(
|
||||||
|
Command::new("systemctl")
|
||||||
|
.arg("restart")
|
||||||
|
.arg("nix-daemon.service"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
return Err(HarmonicError::InitNotSupported);
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -198,3 +357,15 @@ async fn create_dir_with_permissions(
|
||||||
set_permissions(path, permissions).await?;
|
set_permissions(path, permissions).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn status_failure_as_error(command: &mut Command) -> Result<ExitStatus, HarmonicError> {
|
||||||
|
let command_str = format!("{:?}", command.as_std());
|
||||||
|
let status = command
|
||||||
|
.status()
|
||||||
|
.await
|
||||||
|
.map_err(|e| HarmonicError::CommandFailedExec(command_str.clone(), e))?;
|
||||||
|
match status.success() {
|
||||||
|
true => Ok(status),
|
||||||
|
false => Err(HarmonicError::CommandFailedStatus(command_str)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue