diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a0ba731..b050393c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Setup user environment (#381) + ### Fixed ### Changed diff --git a/Cargo.lock b/Cargo.lock index fe1ffd01..83409736 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -522,6 +522,7 @@ dependencies = [ "tokio", "tokio-retry", "update-informer", + "winapi", "winreg 0.51.0", "xz2", "zip", diff --git a/Cargo.toml b/Cargo.toml index 1b2a7b6a..1aab7586 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ openssl = { version = "0.10.57", features = ["vendored"] } [target.'cfg(windows)'.dependencies] winreg = "0.51.0" +winapi = { version = "0.3.9", features = ["winuser"] } [dev-dependencies] assert_cmd = "2.0.12" diff --git a/README.md b/README.md index 638f7d17..acc170e4 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,11 @@ Options: ### Install Subcommand +> **Warning** +> +> #### Environment modification +> Installation will, by default, modify your environment. If you dont want `espup` to modify your environment, please use the `--no-modify-env` argument. + > **Note** > > #### Xtensa Rust destination path @@ -125,7 +130,7 @@ Options: > **Note** > > #### GitHub API -> During the installation process, several GitHub queries are made, [which are subject to certain limits](https://docs.github.com/en/rest/overview/resources-in-the-rest-api?apiVersion=2022-11-28#rate-limiting). Our number of queries should not hit the limits unless you are running `espup install` command numerous times in a short span of time. We recommend setting the [`GITHUB_TOKEN` environment variable](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret) when using `espup` in CI, if you want to use `espup` on CI, recommend using it via the [`xtensa-toolchain` action](https://github.com/esp-rs/xtensa-toolchain/), and making sure `GITHUB_TOKEN` is not set when using it on a host machine. See https://github.com/esp-rs/xtensa-toolchain/issues/15 for more details on this. +> During the installation process, several GitHub queries are made, [which are subject to certain limits](https://docs.github.com/en/rest/overview/resources-in-the-rest-api?apiVersion=2022-11-28#rate-limiting). Our number of queries should not hit the limit unless you are running `espup install` command numerous times in a short span of time. We recommend setting the [`GITHUB_TOKEN` environment variable](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret) when using `espup` in CI, if you want to use `espup` on CI, recommend using it via the [`xtensa-toolchain` action](https://github.com/esp-rs/xtensa-toolchain/), and making sure `GITHUB_TOKEN` is not set when using it on a host machine. See https://github.com/esp-rs/xtensa-toolchain/issues/15 for more details on this. ``` Usage: espup install [OPTIONS] @@ -136,9 +141,6 @@ Options: [possible values: x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, x86_64-pc-windows-msvc, x86_64-pc-windows-gnu, x86_64-apple-darwin, aarch64-apple-darwin] - -f, --export-file - Relative or full path for the export file that will be generated. If no path is provided, the file will be generated under home directory (https://docs.rs/dirs/latest/dirs/fn.home_dir.html) - -e, --extended-llvm Extends the LLVM installation. @@ -160,6 +162,9 @@ Options: [default: nightly] + -o, --no-modify-env + Don't configure environment variables + -k, --skip-version-parse Skips parsing Xtensa Rust version @@ -202,9 +207,6 @@ Options: [possible values: x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, x86_64-pc-windows-msvc, x86_64-pc-windows-gnu, x86_64-apple-darwin, aarch64-apple-darwin] - -f, --export-file - Relative or full path for the export file that will be generated. If no path is provided, the file will be generated under home directory (https://docs.rs/dirs/latest/dirs/fn.home_dir.html) - -e, --extended-llvm Extends the LLVM installation. @@ -226,6 +228,9 @@ Options: [default: nightly] + -o, --no-modify-env + Don't configure environment variables + -k, --skip-version-parse Skips parsing Xtensa Rust version diff --git a/src/cli.rs b/src/cli.rs index a24730fe..fa1170b0 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,7 +1,9 @@ +//! Command line interface. + use crate::targets::{parse_targets, Target}; use clap::Parser; use clap_complete::Shell; -use std::{collections::HashSet, path::PathBuf}; +use std::collections::HashSet; #[derive(Debug, Parser)] pub struct CompletionsOpts { @@ -17,9 +19,6 @@ pub struct InstallOpts { /// Target triple of the host. #[arg(short = 'd', long, value_parser = ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu", "x86_64-pc-windows-msvc", "x86_64-pc-windows-gnu" , "x86_64-apple-darwin" , "aarch64-apple-darwin"])] pub default_host: Option, - /// Relative or full path for the export file that will be generated. If no path is provided, the file will be generated under home directory (https://docs.rs/dirs/latest/dirs/fn.home_dir.html). - #[arg(short = 'f', long)] - pub export_file: Option, /// Extends the LLVM installation. /// /// This will install the whole LLVM instead of only installing the libs. @@ -34,6 +33,9 @@ pub struct InstallOpts { /// Nightly Rust toolchain version. #[arg(short = 'n', long, default_value = "nightly")] pub nightly_version: String, + /// Don't configure environment variables + #[arg(short = 'o', long)] + pub no_modify_env: bool, /// Skips parsing Xtensa Rust version. #[arg(short = 'k', long)] pub skip_version_parse: bool, diff --git a/src/env.rs b/src/env.rs deleted file mode 100644 index 9b17d915..00000000 --- a/src/env.rs +++ /dev/null @@ -1,165 +0,0 @@ -//! Environment variables set up and export file support. - -use crate::error::Error; -use directories::BaseDirs; -use log::info; -#[cfg(windows)] -use log::warn; -#[cfg(windows)] -use std::env; -use std::{ - fs::File, - io::Write, - path::{Path, PathBuf}, -}; -#[cfg(windows)] -use winreg::{ - enums::{HKEY_CURRENT_USER, KEY_READ, KEY_WRITE}, - RegKey, -}; - -#[cfg(windows)] -const DEFAULT_EXPORT_FILE: &str = "export-esp.ps1"; -#[cfg(not(windows))] -const DEFAULT_EXPORT_FILE: &str = "export-esp.sh"; - -#[cfg(windows)] -/// Sets an environment variable for the current user. -pub fn set_environment_variable(key: &str, value: &str) -> Result<(), Error> { - env::set_var(key, value); - - let hkcu = RegKey::predef(HKEY_CURRENT_USER); - let environment_key = hkcu.open_subkey_with_flags("Environment", KEY_WRITE)?; - environment_key.set_value(key, &value)?; - Ok(()) -} - -#[cfg(windows)] -/// Deletes an environment variable for the current user. -pub fn delete_environment_variable(key: &str) -> Result<(), Error> { - if env::var_os(key).is_none() { - return Ok(()); - } - - env::remove_var(key); - - let hkcu = RegKey::predef(HKEY_CURRENT_USER); - let environment_key = hkcu.open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)?; - environment_key.delete_value(key)?; - Ok(()) -} - -/// Returns the absolute path to the export file, uses the DEFAULT_EXPORT_FILE if no arg is provided. -pub fn get_export_file(export_file: Option) -> Result { - if let Some(export_file) = export_file { - if export_file.is_dir() { - return Err(Error::InvalidDestination(export_file.display().to_string())); - } - if export_file.is_absolute() { - Ok(export_file) - } else { - let current_dir = std::env::current_dir()?; - Ok(current_dir.join(export_file)) - } - } else { - Ok(BaseDirs::new() - .unwrap() - .home_dir() - .join(DEFAULT_EXPORT_FILE)) - } -} - -/// Creates the export file with the necessary environment variables. -pub fn create_export_file(export_file: &PathBuf, exports: &[String]) -> Result<(), Error> { - info!("Creating export file"); - let mut file = File::create(export_file)?; - for e in exports.iter() { - #[cfg(windows)] - let e = e.replace('/', r"\"); - file.write_all(e.as_bytes())?; - file.write_all(b"\n")?; - } - - Ok(()) -} - -/// Instructions to export the environment variables. -pub fn export_environment(export_file: &Path) -> Result<(), Error> { - #[cfg(windows)] - if cfg!(windows) { - set_environment_variable("PATH", &env::var("PATH").unwrap())?; - warn!( - "Your environments variables have been updated! Shell may need to be restarted for changes to be effective" - ); - warn!( - "A file was created at '{}' showing the injected environment variables", - export_file.display() - ); - } - #[cfg(unix)] - if cfg!(unix) { - println!( - "\n\tTo get started, you need to set up some environment variables by running: '. {}'", - export_file.display() - ); - println!( - "\tThis step must be done every time you open a new terminal.\n\t See other methods for setting the environment in https://esp-rs.github.io/book/installation/riscv-and-xtensa.html#3-set-up-the-environment-variables", - ); - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use crate::env::{create_export_file, get_export_file, DEFAULT_EXPORT_FILE}; - use directories::BaseDirs; - use std::{env::current_dir, path::PathBuf}; - - #[test] - #[allow(unused_variables)] - fn test_get_export_file() { - // No arg provided - let home_dir = BaseDirs::new().unwrap().home_dir().to_path_buf(); - let export_file = home_dir.join(DEFAULT_EXPORT_FILE); - assert!(matches!(get_export_file(None), Ok(export_file))); - // Relative path - let current_dir = current_dir().unwrap(); - let export_file = current_dir.join("export.sh"); - assert!(matches!( - get_export_file(Some(PathBuf::from("export.sh"))), - Ok(export_file) - )); - // Absolute path - let export_file = PathBuf::from("/home/user/export.sh"); - assert!(matches!( - get_export_file(Some(PathBuf::from("/home/user/export.sh"))), - Ok(export_file) - )); - // Path is a directory instead of a file - assert!(get_export_file(Some(home_dir)).is_err()); - } - - #[test] - fn test_create_export_file() { - // Creates the export file and writes the correct content to it - let temp_dir = tempfile::TempDir::new().unwrap(); - let export_file = temp_dir.path().join("export.sh"); - let exports = vec![ - "export VAR1=value1".to_string(), - "export VAR2=value2".to_string(), - ]; - create_export_file(&export_file, &exports).unwrap(); - let contents = std::fs::read_to_string(export_file).unwrap(); - assert_eq!(contents, "export VAR1=value1\nexport VAR2=value2\n"); - - // Returns the correct error when it fails to create the export file (it already exists) - let temp_dir = tempfile::TempDir::new().unwrap(); - let export_file = temp_dir.path().join("export.sh"); - std::fs::create_dir_all(&export_file).unwrap(); - let exports = vec![ - "export VAR1=value1".to_string(), - "export VAR2=value2".to_string(), - ]; - assert!(create_export_file(&export_file, &exports).is_err()); - } -} diff --git a/src/env/env.bat b/src/env/env.bat new file mode 100644 index 00000000..e8e76762 --- /dev/null +++ b/src/env/env.bat @@ -0,0 +1,32 @@ +@echo off +rem espup CMD setup + +set XTENSA_GCC={xtensa_gcc} +if not "%XTENSA_GCC%" == "" ( + echo %PATH% | findstr /C:"%XTENSA_GCC%" 1>nul + if errorlevel 1 ( + rem Prepending path + set PATH=%XTENSA_GCC%;%PATH% + ) +) + +set RISCV_GCC={riscv_gcc} +if not "%RISCV_GCC%" == "" ( + echo %PATH% | findstr /C:"%RISCV_GCC%" 1>nul + if errorlevel 1 ( + rem Prepending path + set PATH=%RISCV_GCC%;%PATH% + ) +) + +set LIBCLANG_PATH={libclang_path} +set LIBCLANG_BIN_PATH={libclang_bin_path} +if not "%LIBCLANG_BIN_PATH%" == "" ( + echo %PATH% | findstr /C:"%LIBCLANG_BIN_PATH%" 1>nul + if errorlevel 1 ( + rem Prepending path + set PATH=%LIBCLANG_BIN_PATH%;%PATH% + ) +) + +set CLANG_PATH={clang_path} diff --git a/src/env/env.fish b/src/env/env.fish new file mode 100644 index 00000000..236c7d3a --- /dev/null +++ b/src/env/env.fish @@ -0,0 +1,19 @@ +# espup shell setup +set XTENSA_GCC "{xtensa_gcc}" +if test -n "$XTENSA_GCC" + if not contains "{xtensa_gcc}" $PATH + # Prepending path + set -x PATH "{xtensa_gcc}" $PATH + end +end + +set RISCV_GCC "{riscv_gcc}" +if test -n "$RISCV_GCC" + if not contains "{riscv_gcc}" $PATH + # Prepending path + set -x PATH "{riscv_gcc}" $PATH + end +end + +set -x LIBCLANG_PATH "{libclang_path}" +set -x CLANG_PATH "{clang_path}" diff --git a/src/env/env.ps1 b/src/env/env.ps1 new file mode 100644 index 00000000..0d54e16c --- /dev/null +++ b/src/env/env.ps1 @@ -0,0 +1,28 @@ +# espup PowerShell setup +# affix semicolons on either side of $Env:PATH to simplify matching +$XTENSA_GCC = "{xtensa_gcc}" +if ($XTENSA_GCC -ne "") { + if (-not ($Env:PATH -like "*;$XTENSA_GCC;*")) { + # Prepending path + $Env:PATH = "$XTENSA_GCC;$Env:PATH" + } +} + +$RISCV_GCC = "{riscv_gcc}" +if ($RISCV_GCC -ne "") { + if (-not ($Env:PATH -like "*;$RISCV_GCC;*")) { + # Prepending path + $Env:PATH = "$RISCV_GCC;$Env:PATH" + } +} + +$Env:LIBCLANG_PATH = "{libclang_path}" +$LIBCLANG_BIN_PATH = "{libclang_bin_path}" +if ($LIBCLANG_BIN_PATH -ne "") { + if (-not ($Env:PATH -like "*;$LIBCLANG_BIN_PATH;*")) { + # Prepending path + $Env:PATH = "$LIBCLANG_BIN_PATH;$Env:PATH" + } +} + +$Env:CLANG_PATH = "{clang_path}" diff --git a/src/env/env.sh b/src/env/env.sh new file mode 100644 index 00000000..a13b984d --- /dev/null +++ b/src/env/env.sh @@ -0,0 +1,25 @@ +#!/bin/sh +# espup shell setup +# affix colons on either side of $PATH to simplify matching +XTENSA_GCC="{xtensa_gcc}" +if [[ -n "${XTENSA_GCC}" ]]; then + case ":${PATH}:" in + *:"{xtensa_gcc}":*) ;; + *) + # Prepending path + export PATH="{xtensa_gcc}:$PATH" + ;; + esac +fi +RISCV_GCC="{riscv_gcc}" +if [[ -n "${RISCV_GCC}" ]]; then + case ":${PATH}:" in + *:"{riscv_gcc}":*) ;; + *) + # Prepending path + export PATH="{riscv_gcc}:$PATH" + ;; + esac +fi +export LIBCLANG_PATH="{libclang_path}" +export CLANG_PATH="{clang_path}" diff --git a/src/env/mod.rs b/src/env/mod.rs new file mode 100644 index 00000000..4262f9fc --- /dev/null +++ b/src/env/mod.rs @@ -0,0 +1,66 @@ +//! Environment variables set up and environment file support. + +use crate::error::Error; +use directories::BaseDirs; +use miette::Result; +use std::path::{Path, PathBuf}; + +pub mod shell; +#[cfg(unix)] +pub mod unix; +#[cfg(windows)] +pub mod windows; + +/// Instructions to export the environment variables. +pub fn set_env(toolchain_dir: &Path, no_modify_env: bool) -> Result<(), Error> { + #[cfg(windows)] + windows::write_env_files(toolchain_dir)?; + #[cfg(unix)] + unix::write_env_files(toolchain_dir)?; + + if !no_modify_env { + #[cfg(windows)] + windows::update_env()?; + #[cfg(unix)] + unix::update_env(toolchain_dir)?; + } + + Ok(()) +} + +/// Get the home directory. +pub fn get_home_dir() -> PathBuf { + BaseDirs::new().unwrap().home_dir().to_path_buf() +} + +/// Clean the environment variables. +pub fn clean_env(install_dir: &Path) -> Result<(), Error> { + #[cfg(windows)] + windows::clean_env(install_dir)?; + #[cfg(unix)] + unix::clean_env(install_dir)?; + + Ok(()) +} + +/// Print to post installation message +pub fn print_post_install_msg(toolchain_dir: &str, no_modify_env: bool) { + if no_modify_env { + println!( + "\tTo get started you need to configure some environment variable. This has not been done automatically." + ); + } else { + println!("\tTo get started you may need to restart your current shell."); + } + println!("\tTo configure your current shell, run:"); + #[cfg(unix)] + println!( + "\t'. {}/env' or '. {}/env.fish' depending on your shell", + toolchain_dir, toolchain_dir + ); + #[cfg(windows)] + println!( + "\t'. {}\\env.ps1' or '{}\\env.bat' depending on your shell", + toolchain_dir, toolchain_dir + ); +} diff --git a/src/env/shell.rs b/src/env/shell.rs new file mode 100644 index 00000000..672f4111 --- /dev/null +++ b/src/env/shell.rs @@ -0,0 +1,335 @@ +// THIS FILE IS BASED ON RUSTUP SOURCE CODE: https://github.com/rust-lang/rustup/blob/b900a6cd87e1f463a55ce02e956c24b2cccdd0f0/src/cli/self_update/shell.rs + +//! Paths and Unix shells +//! +//! MacOS, Linux, FreeBSD, and many other OS model their design on Unix, +//! so handling them is relatively consistent. But only relatively. +//! POSIX postdates Unix by 20 years, and each "Unix-like" shell develops +//! unique quirks over time. +//! +//! +//! Windowing Managers, Desktop Environments, GUI Terminals, and PATHs +//! +//! Duplicating paths in PATH can cause performance issues when the OS searches +//! the same place multiple times. Traditionally, Unix configurations have +//! resolved this by setting up PATHs in the shell's login profile. +//! +//! This has its own issues. Login profiles are only intended to run once, but +//! changing the PATH is common enough that people may run it twice. Desktop +//! environments often choose to NOT start login shells in GUI terminals. Thus, +//! a trend has emerged to place PATH updates in other run-commands (rc) files, +//! leaving Rustup with few assumptions to build on for fulfilling its promise +//! to set up PATH appropriately. +//! +//! Rustup addresses this by: +//! 1) using a shell script that updates PATH if the path is not in PATH +//! 2) sourcing this script (`. /path/to/script`) in any appropriate rc file + +#[cfg(unix)] +use crate::env::get_home_dir; +use crate::error::Error; +use miette::Result; +use std::{ + env, + fs::OpenOptions, + io::Write, + path::{Path, PathBuf}, +}; + +#[cfg(unix)] +pub(super) type Shell = Box; +#[cfg(windows)] +pub(super) type Shell = Box; + +#[derive(Debug, PartialEq)] +pub struct ShellScript { + content: &'static str, + name: &'static str, + toolchain_dir: PathBuf, +} + +impl ShellScript { + pub(crate) fn write(&self) -> Result<(), Error> { + let env_file_path = self.toolchain_dir.join(self.name); + let mut env_file: String = self.content.to_string(); + + let xtensa_gcc = env::var("XTENSA_GCC").unwrap_or_default(); + env_file = env_file.replace("{xtensa_gcc}", &xtensa_gcc); + + let riscv_gcc = env::var("RISCV_GCC").unwrap_or_default(); + env_file = env_file.replace("{riscv_gcc}", &riscv_gcc); + + let libclang_path = env::var("LIBCLANG_PATH").unwrap_or_default(); + env_file = env_file.replace("{libclang_path}", &libclang_path); + #[cfg(windows)] + if cfg!(windows) { + let libclang_bin_path = env::var("LIBCLANG_BIN_PATH").unwrap_or_default(); + env_file = env_file.replace("{libclang_bin_path}", &libclang_bin_path); + } + + let clang_path = env::var("CLANG_PATH").unwrap_or_default(); + env_file = env_file.replace("{clang_path}", &clang_path); + + write_file(&env_file_path, &env_file)?; + Ok(()) + } +} + +#[cfg(unix)] +/// Cross-platform non-POSIX shells have not been assessed for integration yet +fn enumerate_shells() -> Vec { + vec![ + Box::new(Posix), + Box::new(Bash), + Box::new(Zsh), + Box::new(Fish), + ] +} + +#[cfg(unix)] +/// Returns all shells that exist on the system. +pub(super) fn get_available_shells() -> impl Iterator { + enumerate_shells().into_iter().filter(|sh| sh.does_exist()) +} + +#[cfg(windows)] +pub trait WindowsShell { + /// Writes the relevant env file. + fn env_script(&self, toolchain_dir: &Path) -> ShellScript; + + /// Gives the source string for a given shell. + fn source_string(&self, toolchain_dir: &str) -> Result; +} + +#[cfg(windows)] +pub struct Batch; +#[cfg(windows)] +impl WindowsShell for Batch { + fn env_script(&self, toolchain_dir: &Path) -> ShellScript { + ShellScript { + name: "env.bat", + content: include_str!("env.bat"), + toolchain_dir: toolchain_dir.to_path_buf(), + } + } + + fn source_string(&self, toolchain_dir: &str) -> Result { + Ok(format!(r#"{}/env.bat""#, toolchain_dir)) + } +} + +#[cfg(windows)] +pub struct Powershell; +#[cfg(windows)] +impl WindowsShell for Powershell { + fn env_script(&self, toolchain_dir: &Path) -> ShellScript { + ShellScript { + name: "env.ps1", + content: include_str!("env.ps1"), + toolchain_dir: toolchain_dir.to_path_buf(), + } + } + + fn source_string(&self, toolchain_dir: &str) -> Result { + Ok(format!(r#". "{}/env.ps1""#, toolchain_dir)) + } +} + +#[cfg(unix)] +pub trait UnixShell { + /// Detects if a shell "exists". Users have multiple shells, so an "eager" + /// heuristic should be used, assuming shells exist if any traces do. + fn does_exist(&self) -> bool; + + /// Gives all rcfiles of a given shell that Rustup is concerned with. + /// Used primarily in checking rcfiles for cleanup. + fn rcfiles(&self) -> Vec; + + /// Gives rcs that should be written to. + fn update_rcs(&self) -> Vec; + + /// Writes the relevant env file. + fn env_script(&self, toolchain_dir: &Path) -> ShellScript { + ShellScript { + name: "env", + content: include_str!("env.sh"), + toolchain_dir: toolchain_dir.to_path_buf(), + } + } + + /// Gives the source string for a given shell. + fn source_string(&self, toolchain_dir: &str) -> Result { + Ok(format!(r#". "{}/env""#, toolchain_dir)) + } +} + +#[cfg(unix)] +struct Posix; +#[cfg(unix)] +impl UnixShell for Posix { + fn does_exist(&self) -> bool { + true + } + + fn rcfiles(&self) -> Vec { + vec![get_home_dir().join(".profile")] + } + + fn update_rcs(&self) -> Vec { + // Write to .profile even if it doesn't exist. It's the only rc in the + // POSIX spec so it should always be set up. + self.rcfiles() + } +} + +#[cfg(unix)] +struct Bash; +#[cfg(unix)] +impl UnixShell for Bash { + fn does_exist(&self) -> bool { + !self.update_rcs().is_empty() + } + + fn rcfiles(&self) -> Vec { + // Bash also may read .profile, however Rustup already includes handling + // .profile as part of POSIX and always does setup for POSIX shells. + [".bash_profile", ".bash_login", ".bashrc"] + .iter() + .map(|rc| get_home_dir().join(rc)) + .collect() + } + + fn update_rcs(&self) -> Vec { + self.rcfiles() + .into_iter() + .filter(|rc| rc.is_file()) + .collect() + } +} + +#[cfg(unix)] +struct Zsh; +#[cfg(unix)] +impl Zsh { + fn zdotdir() -> Result { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + if matches!(env::var("SHELL"), Ok(sh) if sh.contains("zsh")) { + match env::var("ZDOTDIR") { + Ok(dir) if !dir.is_empty() => Ok(PathBuf::from(dir)), + _ => Err(Error::Zdotdir), + } + } else { + match std::process::Command::new("zsh") + .args(["-c", "'echo $ZDOTDIR'"]) + .output() + { + Ok(io) if !io.stdout.is_empty() => Ok(PathBuf::from(OsStr::from_bytes(&io.stdout))), + _ => Err(Error::Zdotdir), + } + } + } +} + +#[cfg(unix)] +impl UnixShell for Zsh { + fn does_exist(&self) -> bool { + // zsh has to either be the shell or be callable for zsh setup. + matches!(env::var("SHELL"), Ok(sh) if sh.contains("zsh")) || find_cmd(&["zsh"]).is_some() + } + + fn rcfiles(&self) -> Vec { + let home_dir: Option = Some(get_home_dir()); + [Zsh::zdotdir().ok(), home_dir] + .iter() + .filter_map(|dir| dir.as_ref().map(|p| p.join(".zshenv"))) + .collect() + } + + fn update_rcs(&self) -> Vec { + // zsh can change $ZDOTDIR both _before_ AND _during_ reading .zshenv, + // so we: write to $ZDOTDIR/.zshenv if-exists ($ZDOTDIR changes before) + // OR write to $HOME/.zshenv if it exists (change-during) + // if neither exist, we create it ourselves, but using the same logic, + // because we must still respond to whether $ZDOTDIR is set or unset. + // In any case we only write once. + self.rcfiles() + .into_iter() + .filter(|env| env.is_file()) + .chain(self.rcfiles()) + .take(1) + .collect() + } +} + +#[cfg(unix)] +struct Fish; +#[cfg(unix)] +impl UnixShell for Fish { + fn does_exist(&self) -> bool { + // fish has to either be the shell or be callable for fish setup. + matches!(env::var("SHELL"), Ok(sh) if sh.contains("fish")) || find_cmd(&["fish"]).is_some() + } + + // > "$XDG_CONFIG_HOME/fish/conf.d" (or "~/.config/fish/conf.d" if that variable is unset) for the user + // from + fn rcfiles(&self) -> Vec { + let p0 = env::var("XDG_CONFIG_HOME").ok().map(|p| { + let mut path = PathBuf::from(p); + path.push("fish/conf.d/espup.fish"); + path + }); + + let p1 = get_home_dir().join(".config/fish/conf.d/espup.fish"); + + p0.into_iter().chain(Some(p1)).collect() + } + + fn update_rcs(&self) -> Vec { + self.rcfiles() + } + + fn env_script(&self, toolchain_dir: &Path) -> ShellScript { + ShellScript { + name: "env.fish", + content: include_str!("env.fish"), + toolchain_dir: toolchain_dir.to_path_buf(), + } + } + + fn source_string(&self, toolchain_dir: &str) -> Result { + Ok(format!(r#". "{}/env.fish""#, toolchain_dir)) + } +} + +#[cfg(unix)] +/// Finds the command for a given string. +pub(crate) fn find_cmd<'a>(cmds: &[&'a str]) -> Option<&'a str> { + cmds.iter().cloned().find(|&s| has_cmd(s)) +} + +#[cfg(unix)] +/// Checks if a command exists in the PATH. +fn has_cmd(cmd: &str) -> bool { + let cmd = format!("{}{}", cmd, env::consts::EXE_SUFFIX); + let path = env::var("PATH").unwrap_or_default(); + env::split_paths(&path) + .map(|p| p.join(&cmd)) + .any(|p| p.exists()) +} + +/// Writes a file to a given path. +pub fn write_file(path: &Path, contents: &str) -> Result<(), Error> { + let mut file = OpenOptions::new() + .write(true) + .truncate(true) + .create(true) + .open(path)?; + + Write::write_all(&mut file, contents.as_bytes())?; + + file.sync_data()?; + + Ok(()) +} diff --git a/src/env/unix.rs b/src/env/unix.rs new file mode 100644 index 00000000..a0531008 --- /dev/null +++ b/src/env/unix.rs @@ -0,0 +1,113 @@ +//! Unix specific environment functions. + +use crate::{env::get_home_dir, env::shell, error::Error}; +use miette::Result; +use std::{ + fs::{remove_file, OpenOptions}, + io::Write, + path::{Path, PathBuf}, +}; + +const LEGACY_EXPORT_FILE: &str = "export-esp.sh"; + +/// Clean the environment for Windows. +pub(super) fn clean_env(toolchain_dir: &Path) -> Result<(), Error> { + for sh in shell::get_available_shells() { + let source_bytes = format!( + "{}\n", + sh.source_string(&toolchain_dir.display().to_string())? + ) + .into_bytes(); + + // Check more files for cleanup than normally are updated. + for rc in sh.rcfiles().iter().filter(|rc| rc.is_file()) { + let file = std::fs::read_to_string(rc).map_err(|_| Error::ReadingFile { + name: "rcfile", + path: PathBuf::from(&rc), + })?; + let file_bytes = file.into_bytes(); + // FIXME: This is whitespace sensitive where it should not be. + if let Some(idx) = file_bytes + .windows(source_bytes.len()) + .position(|w| w == source_bytes.as_slice()) + { + // Here we rewrite the file without the offending line. + let mut new_bytes = file_bytes[..idx].to_vec(); + new_bytes.extend(&file_bytes[idx + source_bytes.len()..]); + let new_file = String::from_utf8(new_bytes).unwrap(); + let mut file = OpenOptions::new() + .write(true) + .truncate(true) + .create(true) + .open(rc)?; + Write::write_all(&mut file, new_file.as_bytes())?; + + file.sync_data()?; + } + } + } + + remove_legacy_export_file()?; + + Ok(()) +} + +/// Delete the legacy export file. +fn remove_legacy_export_file() -> Result<(), Error> { + let legacy_file = get_home_dir().join(LEGACY_EXPORT_FILE); + if legacy_file.exists() { + remove_file(&legacy_file)?; + } + + Ok(()) +} + +/// Update the environment for Unix. +pub(crate) fn update_env(toolchain_dir: &Path) -> Result<(), Error> { + for sh in shell::get_available_shells() { + let source_cmd = sh.source_string(&toolchain_dir.display().to_string())?; + let source_cmd_with_newline = format!("\n{}", &source_cmd); + + for rc in sh.update_rcs() { + let file = std::fs::read_to_string(&rc).map_err(|_| Error::ReadingFile { + name: "rcfile", + path: PathBuf::from(&rc), + }); + let cmd_to_write: &str = match file { + Ok(contents) if contents.contains(&source_cmd) => continue, + Ok(contents) if !contents.ends_with('\n') => &source_cmd_with_newline, + _ => &source_cmd, + }; + + let mut dest_file = OpenOptions::new() + .write(true) + .append(true) + .create(true) + .open(&rc)?; + + writeln!(dest_file, "{cmd_to_write}")?; + + dest_file.sync_data()?; + } + } + + remove_legacy_export_file()?; + + Ok(()) +} + +/// Write the environment files for Unix. +pub(super) fn write_env_files(toolchain_dir: &Path) -> Result<(), Error> { + let mut written = vec![]; + + for sh in shell::get_available_shells() { + let script = sh.env_script(toolchain_dir); + // Only write each possible script once. + if !written.contains(&script) { + script.write()?; + written.push(script); + } + } + + Ok(()) +} diff --git a/src/env/windows.rs b/src/env/windows.rs new file mode 100644 index 00000000..6d64181a --- /dev/null +++ b/src/env/windows.rs @@ -0,0 +1,138 @@ +//! Windows specific environment functions. + +use crate::{env::get_home_dir, env::shell, error::Error}; +use miette::Result; +use std::{env, fs::remove_file, path::Path}; +use winreg::{ + enums::{HKEY_CURRENT_USER, KEY_READ, KEY_WRITE}, + RegKey, +}; + +const LEGACY_EXPORT_FILE: &str = "export-esp.ps1"; + +/// Clean the environment for Windows. +pub(super) fn clean_env(_install_dir: &Path) -> Result<(), Error> { + delete_env_variable("LIBCLANG_PATH")?; + delete_env_variable("CLANG_PATH")?; + if let Some(path) = env::var_os("PATH") { + set_env_variable("PATH", &path.to_string_lossy())?; + }; + + remove_legacy_export_file()?; + + Ok(()) +} + +/// Deletes an environment variable for the current user. +fn delete_env_variable(key: &str) -> Result<(), Error> { + let root = RegKey::predef(HKEY_CURRENT_USER); + let environment = root.open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)?; + + let reg_value = environment.get_raw_value(key); + if reg_value.is_err() { + return Ok(()); + } + + env::remove_var(key); + + let hkcu = RegKey::predef(HKEY_CURRENT_USER); + let environment_key = hkcu.open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)?; + environment_key.delete_value(key)?; + Ok(()) +} + +/// Sets an environment variable for the current user. +fn set_env_variable(key: &str, value: &str) -> Result<(), Error> { + use std::ptr; + use winapi::shared::minwindef::*; + use winapi::um::winuser::{ + SendMessageTimeoutA, HWND_BROADCAST, SMTO_ABORTIFHUNG, WM_SETTINGCHANGE, + }; + + env::set_var(key, value); + + let hkcu = RegKey::predef(HKEY_CURRENT_USER); + let environment_key = hkcu.open_subkey_with_flags("Environment", KEY_WRITE)?; + environment_key.set_value(key, &value)?; + + // Tell other processes to update their environment + #[allow(clippy::unnecessary_cast)] + unsafe { + SendMessageTimeoutA( + HWND_BROADCAST, + WM_SETTINGCHANGE, + 0 as WPARAM, + "Environment\0".as_ptr() as LPARAM, + SMTO_ABORTIFHUNG, + 5000, + ptr::null_mut(), + ); + } + + Ok(()) +} + +/// Delete the legacy export file. +fn remove_legacy_export_file() -> Result<(), Error> { + let legacy_file = get_home_dir().join(LEGACY_EXPORT_FILE); + if legacy_file.exists() { + remove_file(&legacy_file)?; + } + + Ok(()) +} + +// Update the environment for Windows. +pub(super) fn update_env() -> Result<(), Error> { + let mut path = env::var("PATH").unwrap_or_default(); + + if let Ok(xtensa_gcc) = env::var("XTENSA_GCC") { + let xtensa_gcc: &str = &xtensa_gcc; + if !path.contains(xtensa_gcc) { + path = format!("{};{}", xtensa_gcc, path); + } + } + + if let Ok(riscv_gcc) = env::var("RISCV_GCC") { + let riscv_gcc: &str = &riscv_gcc; + if !path.contains(riscv_gcc) { + path = format!("{};{}", riscv_gcc, path); + } + } + + if let Ok(libclang_path) = env::var("LIBCLANG_PATH") { + set_env_variable("LIBCLANG_PATH", &libclang_path)?; + } + + if let Ok(libclang_bin_path) = env::var("LIBCLANG_BIN_PATH") { + let libclang_bin_path: &str = &libclang_bin_path; + if !path.contains(libclang_bin_path) { + path = format!("{};{}", libclang_bin_path, path); + } + } + + if let Ok(clang_path) = env::var("CLANG_PATH") { + let clang_path: &str = &clang_path; + if !path.contains(clang_path) { + path = format!("{};{}", clang_path, path); + } + } + + set_env_variable("PATH", &path)?; + + remove_legacy_export_file()?; + + Ok(()) +} + +/// Write the environment files for Windows. +pub(super) fn write_env_files(toolchain_dir: &Path) -> Result<(), Error> { + let windows_shells: Vec = + vec![Box::new(shell::Batch), Box::new(shell::Powershell)]; + for sh in windows_shells.into_iter() { + let script = sh.env_script(toolchain_dir); + script.write()?; + } + + Ok(()) +} diff --git a/src/error.rs b/src/error.rs index d3452070..c9afb007 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,7 @@ //! Custom error implementations. +use std::path::PathBuf; + #[derive(Debug, miette::Diagnostic, thiserror::Error)] pub enum Error { #[diagnostic(code(espup::toolchain::create_directory))] @@ -19,6 +21,10 @@ pub enum Error { "Invalid export file destination: '{0}'. Please, use an absolute or releative path (including the file and its extension)")] InvalidDestination(String), + #[diagnostic(code(espup::env::home_dir))] + #[error("Failed to query GitHub API")] + InvalidHome, + #[diagnostic(code(espup::toolchain::rust::invalid_version))] #[error( "Invalid toolchain version '{0}'. Verify that the format is correct: '...' or '..', and that the release exists in https://github.com/esp-rs/rust-build/releases")] @@ -69,4 +75,12 @@ pub enum Error { #[diagnostic(code(espup::toolchain::rust::rust_src))] #[error("Failed to install 'rust-src' component of Xtensa Rust")] XtensaRustSrc, + + #[diagnostic(code(espup::env::unix))] + #[error("Failed to read {name} file: '{}'", .path.display())] + ReadingFile { name: &'static str, path: PathBuf }, + + #[diagnostic(code(espup::env::shell))] + #[error("ZDOTDIR not set")] + Zdotdir, } diff --git a/src/main.rs b/src/main.rs index ba5b2618..9004d84c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,7 @@ use clap::{CommandFactory, Parser}; -#[cfg(windows)] -use espup::env::set_environment_variable; use espup::{ cli::{CompletionsOpts, InstallOpts, UninstallOpts}, + env::clean_env, error::Error, logging::initialize_logger, toolchain::{ @@ -69,21 +68,22 @@ async fn uninstall(args: UninstallOpts) -> Result<()> { check_for_update(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); info!("Uninstalling the Espressif Rust ecosystem"); - let install_path = get_rustup_home().join("toolchains").join(args.name); + let install_dir = get_rustup_home().join("toolchains").join(args.name); - Llvm::uninstall(&install_path)?; + Llvm::uninstall(&install_dir)?; - uninstall_gcc_toolchains(&install_path)?; + uninstall_gcc_toolchains(&install_dir)?; - info!( - "Deleting the Xtensa Rust toolchain located in '{}'", - &install_path.display() - ); - remove_dir_all(&install_path) - .map_err(|_| Error::RemoveDirectory(install_path.display().to_string()))?; + if install_dir.exists() { + info!( + "Deleting the Xtensa Rust toolchain located in '{}'", + &install_dir.display() + ); + remove_dir_all(&install_dir) + .map_err(|_| Error::RemoveDirectory(install_dir.display().to_string()))?; + } - #[cfg(windows)] - set_environment_variable("PATH", &env::var("PATH").unwrap())?; + clean_env(&install_dir)?; info!("Uninstallation successfully completed!"); Ok(()) diff --git a/src/toolchain/gcc.rs b/src/toolchain/gcc.rs index 53130c44..82046cf1 100644 --- a/src/toolchain/gcc.rs +++ b/src/toolchain/gcc.rs @@ -9,6 +9,7 @@ use async_trait::async_trait; use log::{debug, info, warn}; use miette::Result; use std::{ + env, fs::remove_dir_all, path::{Path, PathBuf}, }; @@ -30,8 +31,13 @@ pub struct Gcc { impl Gcc { /// Gets the binary path. - pub fn get_bin_path(&self) -> String { - format!("{}/{}/bin", &self.path.to_str().unwrap(), &self.arch) + fn get_bin_path(&self) -> String { + #[cfg(windows)] + let bin_path = + format!("{}/{}/bin", &self.path.to_str().unwrap(), &self.arch).replace('/', "\\"); + #[cfg(unix)] + let bin_path = format!("{}/{}/bin", &self.path.to_str().unwrap(), &self.arch); + bin_path } /// Create a new instance with default values and proper toolchain name. @@ -50,8 +56,9 @@ impl Gcc { #[async_trait] impl Installable for Gcc { - async fn install(&self) -> Result, Error> { + async fn install(&self) -> Result<(), Error> { let extension = get_artifact_extension(&self.host_triple); + info!("Installing GCC ({})", self.arch); debug!("GCC path: {}", self.path.display()); if self.path.exists() { warn!( @@ -79,23 +86,14 @@ impl Installable for Gcc { ) .await?; } - let mut exports: Vec = Vec::new(); - #[cfg(windows)] - if cfg!(windows) { - exports.push(format!( - "$Env:PATH = \"{};\" + $Env:PATH", - &self.get_bin_path() - )); - std::env::set_var( - "PATH", - self.get_bin_path().replace('/', "\\") + ";" + &std::env::var("PATH").unwrap(), - ); + if self.arch == RISCV_GCC { + env::set_var("RISCV_GCC", self.get_bin_path()); + } else { + env::set_var("XTENSA_GCC", self.get_bin_path()); } - #[cfg(unix)] - exports.push(format!("export PATH=\"{}:$PATH\"", &self.get_bin_path())); - Ok(exports) + Ok(()) } fn name(&self) -> String { @@ -141,9 +139,9 @@ pub fn uninstall_gcc_toolchains(toolchain_path: &Path) -> Result<(), Error> { DEFAULT_GCC_RELEASE, toolchain ); - std::env::set_var( + env::set_var( "PATH", - std::env::var("PATH") + env::var("PATH") .unwrap() .replace(&format!("{gcc_path};"), ""), ); diff --git a/src/toolchain/llvm.rs b/src/toolchain/llvm.rs index 7998c6e5..c25bcfb6 100644 --- a/src/toolchain/llvm.rs +++ b/src/toolchain/llvm.rs @@ -1,7 +1,5 @@ //! LLVM Toolchain source and installation tools. -#[cfg(windows)] -use crate::env::{delete_environment_variable, set_environment_variable}; use crate::{ error::Error, host_triple::HostTriple, @@ -12,6 +10,7 @@ use log::{info, warn}; use miette::Result; use regex::Regex; use std::{ + env, fs::remove_dir_all, path::{Path, PathBuf}, u8, @@ -53,7 +52,11 @@ impl Llvm { /// Gets the binary path. fn get_lib_path(&self) -> String { #[cfg(windows)] - let llvm_path = format!("{}/esp-clang/bin", self.path.to_str().unwrap()); + let llvm_path = format!( + "{}/esp-clang/bin", + self.path.to_str().unwrap().replace('/', "\\") + ) + .replace('/', "\\"); #[cfg(unix)] let llvm_path = format!("{}/esp-clang/lib", self.path.to_str().unwrap()); llvm_path @@ -62,7 +65,8 @@ impl Llvm { /// Gets the binary path of clang fn get_bin_path(&self) -> String { #[cfg(windows)] - let llvm_path = format!("{}/esp-clang/bin/clang.exe", self.path.to_str().unwrap()); + let llvm_path = format!("{}\\esp-clang\\bin\\clang.exe", self.path.to_str().unwrap()) + .replace('/', "\\"); #[cfg(unix)] let llvm_path = format!("{}/esp-clang/bin/clang", self.path.to_str().unwrap()); llvm_path @@ -123,9 +127,9 @@ impl Llvm { if llvm_path.exists() { #[cfg(windows)] if cfg!(windows) { - delete_environment_variable("LIBCLANG_PATH")?; - delete_environment_variable("CLANG_PATH")?; - let updated_path = std::env::var("PATH").unwrap().replace( + env::remove_var("LIBCLANG_PATH"); + env::remove_var("CLANG_PATH"); + let mut updated_path = env::var("PATH").unwrap().replace( &format!( "{}\\{}\\esp-clang\\bin;", llvm_path.display().to_string().replace('/', "\\"), @@ -133,11 +137,18 @@ impl Llvm { ), "", ); - set_environment_variable("PATH", &updated_path)?; + updated_path = updated_path.replace( + &format!( + "{}\\{}\\esp-clang\\bin;", + llvm_path.display().to_string().replace('/', "\\"), + DEFAULT_LLVM_16_VERSION, + ), + "", + ); + env::set_var("PATH", updated_path); } - let path = toolchain_path.join(CLANG_NAME); - remove_dir_all(&path) - .map_err(|_| Error::RemoveDirectory(path.display().to_string()))?; + remove_dir_all(&llvm_path) + .map_err(|_| Error::RemoveDirectory(llvm_path.display().to_string()))?; } Ok(()) } @@ -145,9 +156,7 @@ impl Llvm { #[async_trait] impl Installable for Llvm { - async fn install(&self) -> Result, Error> { - let mut exports: Vec = Vec::new(); - + async fn install(&self) -> Result<(), Error> { if Path::new(&self.path).exists() { warn!( "Previous installation of LLVM exists in: '{}'. Reusing this installation", @@ -166,39 +175,13 @@ impl Installable for Llvm { } // Set environment variables. #[cfg(windows)] - if cfg!(windows) { - exports.push(format!( - "$Env:LIBCLANG_PATH = \"{}/libclang.dll\"", - self.get_lib_path() - )); - exports.push(format!( - "$Env:PATH = \"{};\" + $Env:PATH", - self.get_lib_path() - )); - set_environment_variable( - "LIBCLANG_PATH", - &format!("{}\\libclang.dll", self.get_lib_path().replace('/', "\\")), - )?; - - std::env::set_var( - "PATH", - self.get_lib_path().replace('/', "\\") + ";" + &std::env::var("PATH").unwrap(), - ); - } - #[cfg(unix)] - exports.push(format!("export LIBCLANG_PATH=\"{}\"", self.get_lib_path())); - + env::set_var("LIBCLANG_BIN_PATH", self.get_lib_path()); + env::set_var("LIBCLANG_PATH", self.get_lib_path()); if self.extended { - #[cfg(windows)] - if cfg!(windows) { - exports.push(format!("$Env:CLANG_PATH = \"{}\"", self.get_bin_path())); - set_environment_variable("CLANG_PATH", &self.get_bin_path().replace('/', "\\"))?; - } - #[cfg(unix)] - exports.push(format!("export CLANG_PATH=\"{}\"", self.get_bin_path())); + env::set_var("CLANG_PATH", self.get_bin_path()); } - Ok(exports) + Ok(()) } fn name(&self) -> String { diff --git a/src/toolchain/mod.rs b/src/toolchain/mod.rs index efc1f2b6..3c84c4bd 100644 --- a/src/toolchain/mod.rs +++ b/src/toolchain/mod.rs @@ -2,7 +2,7 @@ use crate::{ cli::InstallOpts, - env::{create_export_file, export_environment, get_export_file}, + env::{print_post_install_msg, set_env}, error::Error, host_triple::get_host_triple, targets::Target, @@ -41,14 +41,14 @@ pub enum InstallMode { #[async_trait] pub trait Installable { - /// Install some application, returning a vector of any required exports - async fn install(&self) -> Result, Error>; + /// Install some application + async fn install(&self) -> Result<(), Error>; /// Returns the name of the toolchain being installeds fn name(&self) -> String; } /// Downloads a file from a URL and uncompresses it, if necesary, to the output directory. -pub async fn download_file( +pub(super) async fn download_file( url: String, file_name: &str, output_directory: &str, @@ -63,11 +63,11 @@ pub async fn download_file( ); remove_file(&file_path)?; } else if !Path::new(&output_directory).exists() { - info!("Creating directory: '{}'", output_directory); + debug!("Creating directory: '{}'", output_directory); create_dir_all(output_directory) .map_err(|_| Error::CreateDirectory(output_directory.to_string()))?; } - info!("Downloading file '{}' from '{}'", &file_path, url); + info!("Downloading '{}'", &file_name); let resp = reqwest::get(&url).await?; let bytes = resp.bytes().await?; if uncompress { @@ -101,7 +101,7 @@ pub async fn download_file( } } "gz" => { - info!("Extracting tar.gz file to '{}'", output_directory); + debug!("Extracting tar.gz file to '{}'", output_directory); let bytes = bytes.to_vec(); let tarfile = GzDecoder::new(bytes.as_slice()); @@ -109,7 +109,7 @@ pub async fn download_file( archive.unpack(output_directory)?; } "xz" => { - info!("Extracting tar.xz file to '{}'", output_directory); + debug!("Extracting tar.xz file to '{}'", output_directory); let bytes = bytes.to_vec(); let tarfile = XzDecoder::new(bytes.as_slice()); let mut archive = Archive::new(tarfile); @@ -120,7 +120,7 @@ pub async fn download_file( } } } else { - info!("Creating file: '{}'", file_path); + debug!("Creating file: '{}'", file_path); let mut out = File::create(&file_path)?; out.write_all(&bytes)?; } @@ -133,8 +133,6 @@ pub async fn install(args: InstallOpts, install_mode: InstallMode) -> Result<()> InstallMode::Install => info!("Installing the Espressif Rust ecosystem"), InstallMode::Update => info!("Updating the Espressif Rust ecosystem"), } - let export_file = get_export_file(args.export_file)?; - let mut exports: Vec = Vec::new(); let host_triple = get_host_triple(args.default_host)?; let xtensa_rust_version = if let Some(toolchain_version) = &args.toolchain_version { if !args.skip_version_parse { @@ -145,9 +143,9 @@ pub async fn install(args: InstallOpts, install_mode: InstallMode) -> Result<()> } else { XtensaRust::get_latest_version().await? }; - let install_path = get_rustup_home().join("toolchains").join(args.name); + let toolchain_dir = get_rustup_home().join("toolchains").join(args.name); let llvm: Llvm = Llvm::new( - &install_path, + &toolchain_dir, &host_triple, args.extended_llvm, &xtensa_rust_version, @@ -160,7 +158,7 @@ pub async fn install(args: InstallOpts, install_mode: InstallMode) -> Result<()> Some(XtensaRust::new( &xtensa_rust_version, &host_triple, - &install_path, + &toolchain_dir, )) } else { None @@ -168,7 +166,6 @@ pub async fn install(args: InstallOpts, install_mode: InstallMode) -> Result<()> debug!( "Arguments: - - Export file: {:?} - Host triple: {} - LLVM Toolchain: {:?} - Nightly version: {:?} @@ -177,14 +174,13 @@ pub async fn install(args: InstallOpts, install_mode: InstallMode) -> Result<()> - Targets: {:?} - Toolchain path: {:?} - Toolchain version: {:?}", - &export_file, host_triple, &llvm, &args.nightly_version, xtensa_rust, &args.skip_version_parse, targets, - &install_path, + &toolchain_dir, args.toolchain_version, ); @@ -210,20 +206,20 @@ pub async fn install(args: InstallOpts, install_mode: InstallMode) -> Result<()> .iter() .any(|t| t == &Target::ESP32 || t == &Target::ESP32S2 || t == &Target::ESP32S3) { - let xtensa_gcc = Gcc::new(XTENSA_GCC, &host_triple, &install_path); + let xtensa_gcc = Gcc::new(XTENSA_GCC, &host_triple, &toolchain_dir); to_install.push(Box::new(xtensa_gcc)); } // All RISC-V targets use the same GCC toolchain // ESP32S2 and ESP32S3 also install the RISC-V toolchain for their ULP coprocessor if targets.iter().any(|t| t != &Target::ESP32) { - let riscv_gcc = Gcc::new(RISCV_GCC, &host_triple, &install_path); + let riscv_gcc = Gcc::new(RISCV_GCC, &host_triple, &toolchain_dir); to_install.push(Box::new(riscv_gcc)); } } // With a list of applications to install, install them all in parallel. let installable_items = to_install.len(); - let (tx, mut rx) = mpsc::channel::, Error>>(installable_items); + let (tx, mut rx) = mpsc::channel::>(installable_items); for app in to_install { let tx = tx.clone(); let retry_strategy = FixedInterval::from_millis(50).take(3); @@ -242,22 +238,24 @@ pub async fn install(args: InstallOpts, install_mode: InstallMode) -> Result<()> // Read the results of the install tasks as they complete. for _ in 0..installable_items { - let names = rx.recv().await.unwrap()?; - exports.extend(names); + rx.recv().await.unwrap()?; } - create_export_file(&export_file, &exports)?; match install_mode { InstallMode::Install => info!("Installation successfully completed!"), InstallMode::Update => info!("Update successfully completed!"), } - export_environment(&export_file)?; + + set_env(&toolchain_dir, args.no_modify_env)?; + + print_post_install_msg(&toolchain_dir.display().to_string(), args.no_modify_env); + Ok(()) } /// Queries the GitHub API and returns the JSON response. -pub fn github_query(url: &str) -> Result { - info!("Querying GitHub API: '{}'", url); +pub(super) fn github_query(url: &str) -> Result { + debug!("Querying GitHub API: '{}'", url); let mut headers = header::HeaderMap::new(); headers.insert(header::USER_AGENT, "espup".parse().unwrap()); headers.insert( diff --git a/src/toolchain/rust.rs b/src/toolchain/rust.rs index c556cd61..4d83e205 100644 --- a/src/toolchain/rust.rs +++ b/src/toolchain/rust.rs @@ -1,6 +1,7 @@ //! Xtensa Rust Toolchain source and installation tools. use crate::{ + env::get_home_dir, error::Error, host_triple::HostTriple, toolchain::{ @@ -12,7 +13,6 @@ use crate::{ }, }; use async_trait::async_trait; -use directories::BaseDirs; use log::{debug, info, warn}; use miette::Result; use regex::Regex; @@ -182,7 +182,7 @@ impl XtensaRust { #[async_trait] impl Installable for XtensaRust { - async fn install(&self) -> Result, Error> { + async fn install(&self) -> Result<(), Error> { if self.toolchain_destination.exists() { let toolchain_name = format!( "+{}", @@ -203,7 +203,7 @@ impl Installable for XtensaRust { &self.version, &self.toolchain_destination.display() ); - return Ok(vec![]); + return Ok(()); } else { if !rustc_version.status.success() { warn!("Failed to detect version of Xtensa Rust, reinstalling it"); @@ -299,7 +299,7 @@ impl Installable for XtensaRust { .await?; } - Ok(vec![]) // No exports + Ok(()) } fn name(&self) -> String { @@ -346,7 +346,7 @@ impl RiscVTarget { #[async_trait] impl Installable for RiscVTarget { - async fn install(&self) -> Result, Error> { + async fn install(&self) -> Result<(), Error> { info!( "Installing RISC-V Rust targets ('riscv32imc-unknown-none-elf' and 'riscv32imac-unknown-none-elf') for '{}' toolchain", &self.nightly_version ); @@ -372,7 +372,7 @@ impl Installable for RiscVTarget { return Err(Error::InstallRiscvTarget(self.nightly_version.clone())); } - Ok(vec![]) // No exports + Ok(()) } fn name(&self) -> String { @@ -390,30 +390,22 @@ fn get_artifact_extension(host_triple: &HostTriple) -> &str { /// Gets the default cargo home path. fn get_cargo_home() -> PathBuf { - PathBuf::from(env::var("CARGO_HOME").unwrap_or_else(|_e| { - format!( - "{}", - BaseDirs::new().unwrap().home_dir().join(".cargo").display() - ) - })) + PathBuf::from( + env::var("CARGO_HOME") + .unwrap_or_else(|_e| format!("{}", get_home_dir().join(".cargo").display())), + ) } /// Gets the default rustup home path. pub fn get_rustup_home() -> PathBuf { - PathBuf::from(env::var("RUSTUP_HOME").unwrap_or_else(|_e| { - format!( - "{}", - BaseDirs::new() - .unwrap() - .home_dir() - .join(".rustup") - .display() - ) - })) + PathBuf::from( + env::var("RUSTUP_HOME") + .unwrap_or_else(|_e| format!("{}", get_home_dir().join(".rustup").display())), + ) } /// Checks if rustup is installed. -pub async fn check_rust_installation() -> Result<(), Error> { +pub(super) async fn check_rust_installation() -> Result<(), Error> { info!("Checking Rust installation"); if let Err(e) = Command::new("rustup") @@ -434,10 +426,11 @@ pub async fn check_rust_installation() -> Result<(), Error> { #[cfg(test)] mod tests { use crate::{ + env::get_home_dir, logging::initialize_logger, toolchain::rust::{get_cargo_home, get_rustup_home, XtensaRust}, }; - use directories::BaseDirs; + use std::env; #[test] fn test_xtensa_rust_parse_version() { @@ -459,30 +452,24 @@ mod tests { #[test] fn test_get_cargo_home() { // No CARGO_HOME set - std::env::remove_var("CARGO_HOME"); - assert_eq!( - get_cargo_home(), - BaseDirs::new().unwrap().home_dir().join(".cargo") - ); + env::remove_var("CARGO_HOME"); + assert_eq!(get_cargo_home(), get_home_dir().join(".cargo")); // CARGO_HOME set let temp_dir = tempfile::TempDir::new().unwrap(); let cargo_home = temp_dir.path().to_path_buf(); - std::env::set_var("CARGO_HOME", cargo_home.to_str().unwrap()); + env::set_var("CARGO_HOME", cargo_home.to_str().unwrap()); assert_eq!(get_cargo_home(), cargo_home); } #[test] fn test_get_rustup_home() { // No RUSTUP_HOME set - std::env::remove_var("RUSTUP_HOME"); - assert_eq!( - get_rustup_home(), - BaseDirs::new().unwrap().home_dir().join(".rustup") - ); + env::remove_var("RUSTUP_HOME"); + assert_eq!(get_rustup_home(), get_home_dir().join(".rustup")); // RUSTUP_HOME set let temp_dir = tempfile::TempDir::new().unwrap(); let rustup_home = temp_dir.path().to_path_buf(); - std::env::set_var("RUSTUP_HOME", rustup_home.to_str().unwrap()); + env::set_var("RUSTUP_HOME", rustup_home.to_str().unwrap()); assert_eq!(get_rustup_home(), rustup_home); } }