From fe966932ed4ee845b95344b8ffe6226cdfbe6606 Mon Sep 17 00:00:00 2001 From: Ana Hobden Date: Wed, 7 Sep 2022 17:13:06 -0700 Subject: [PATCH] More bones, trimming deps --- Cargo.toml | 9 +- src/cli/mod.rs | 2 + src/error.rs | 31 +++++++ src/interaction.rs | 51 +++-------- src/lib.rs | 209 ++++++++++++++++++++++++++++++++++++++++----- 5 files changed, 244 insertions(+), 58 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 33e3682..a74dc03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/cli/mod.rs b/src/cli/mod.rs index f6219f1..141142d 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -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) } diff --git a/src/error.rs b/src/error.rs index 67f3b70..34c1171 100644 --- a/src/error.rs +++ b/src/error.rs @@ -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), } diff --git a/src/interaction.rs b/src/interaction.rs index c590007..78fdfc4 100644 --- a/src/interaction.rs +++ b/src/interaction.rs @@ -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, - command: Command, -) -> eyre::Result { - 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) -> eyre::Result { let mut stdout = tokio::io::stdout(); @@ -43,19 +24,20 @@ pub(crate) async fn confirm(question: impl AsRef) -> eyre::Result { 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) -> ! { eprintln!("{}", message.as_ref()); std::process::exit(0) } - -pub(crate) async fn angry_bail_with_message(message: impl AsRef, code: i32) -> ! { - eprintln!("{}", message.as_ref()); - std::process::exit(code) -} diff --git a/src/lib.rs b/src/lib.rs index 7e80626..60d9a01 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 { + 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)), + } +}