More bones, trimming deps

This commit is contained in:
Ana Hobden 2022-09-07 17:13:06 -07:00
parent 609ed3563f
commit fe966932ed
5 changed files with 244 additions and 58 deletions

View file

@ -6,7 +6,6 @@ edition = "2021"
resolver = "2"
[dependencies]
async-compression = { version = "0.3.14", features = ["xz", "futures-io"] }
async-tar = "0.4.2"
async-trait = "0.1.57"
atty = "0.2.14"
@ -16,11 +15,17 @@ crossterm = { version = "0.25.0", features = ["event-stream"] }
eyre = "0.6.8"
futures = "0.3.24"
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"
thiserror = "1.0.33"
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-error = "0.2.0"
tracing-subscriber = { version = "0.3.15", features = [ "env-filter", "valuable" ] }
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"

View file

@ -64,6 +64,8 @@ impl CommandExecute for HarmonicCli {
harmonic.place_channel_configuration().await?;
harmonic.fetch_nix().await?;
harmonic.configure_shell_profile().await?;
harmonic.setup_default_profile().await?;
harmonic.place_nix_configuration().await?;
Ok(ExitCode::SUCCESS)
}

View file

@ -16,4 +16,35 @@ pub enum HarmonicError {
CreateDirectory(std::io::Error),
#[error("Placing channel configuration")]
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),
}

View file

@ -2,26 +2,7 @@ use crossterm::event::{EventStream, KeyCode};
use eyre::{eyre, WrapErr};
use futures::{FutureExt, StreamExt};
use owo_colors::OwoColorize;
use tokio::{io::AsyncWriteExt, process::Command};
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
}
use tokio::io::AsyncWriteExt;
pub(crate) async fn confirm(question: impl AsRef<str>) -> eyre::Result<bool> {
let mut stdout = tokio::io::stdout();
@ -43,19 +24,20 @@ pub(crate) async fn confirm(question: impl AsRef<str>) -> eyre::Result<bool> {
loop {
let event = reader.next().fuse().await;
match event {
Some(Ok(event)) => match event {
crossterm::event::Event::Key(key) => match key.code {
KeyCode::Char('y') => break Ok(true),
_ => {
stdout
.write_all("Cancelled!".red().to_string().as_bytes())
.await?;
stdout.flush().await?;
break Ok(false);
Some(Ok(event)) => {
if let crossterm::event::Event::Key(key) = event {
match key.code {
KeyCode::Char('y') => break Ok(true),
_ => {
stdout
.write_all("Cancelled!".red().to_string().as_bytes())
.await?;
stdout.flush().await?;
break Ok(false);
}
}
},
_ => (),
},
}
}
Some(Err(err)) => return Err(err).wrap_err("Getting response"),
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());
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)
}

View file

@ -1,5 +1,10 @@
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;
@ -8,12 +13,14 @@ mod nixos;
#[cfg(target_os = "linux")]
pub use nixos::NixOs;
use futures::stream::TryStreamExt;
use bytes::Buf;
use glob::glob;
use reqwest::Url;
use tokio::{
fs::{create_dir, set_permissions, OpenOptions},
fs::{create_dir, create_dir_all, set_permissions, symlink, OpenOptions},
io::AsyncWriteExt,
process::Command,
task::spawn_blocking,
};
// This uses a Rust builder pattern
@ -54,21 +61,22 @@ impl Harmonic {
)
.await
.map_err(HarmonicError::DownloadingNix)?;
let stream = res.bytes_stream();
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);
let bytes = res.bytes().await.map_err(HarmonicError::DownloadingNix)?;
// TODO(@Hoverbear): Pick directory
let destination = "/nix/store";
archive
.unpack(destination)
.await
.map_err(HarmonicError::UnpackingNix)?;
tracing::debug!(%destination, "Downloaded & extracted Nix");
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
.unpack(destination)
.map_err(HarmonicError::UnpackingNix)?;
tracing::debug!(%destination, "Downloaded & extracted Nix");
Ok(())
})
.await?;
handle?;
Ok(())
}
@ -122,7 +130,7 @@ impl Harmonic {
Ok(())
}
pub async fn create_directories(&self) -> Result<(), HarmonicError> {
let permissions = Permissions::from_mode(755);
let permissions = Permissions::from_mode(0o755);
let paths = [
"/nix",
"/nix/var",
@ -168,7 +176,158 @@ impl Harmonic {
Ok(())
}
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(())
}
}
@ -198,3 +357,15 @@ async fn create_dir_with_permissions(
set_permissions(path, permissions).await?;
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)),
}
}