Skip to content

Commit

Permalink
Implement self-install subcommand, improve error hygiene for linking
Browse files Browse the repository at this point in the history
  • Loading branch information
filiptibell committed Mar 25, 2024
1 parent 950f54f commit 1b315b7
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 54 deletions.
71 changes: 19 additions & 52 deletions lib/storage/tool_storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ use tokio::{
sync::Mutex as AsyncMutex,
task::spawn_blocking,
};
use tracing::debug;

use crate::{
result::AftmanResult,
tool::{ToolAlias, ToolSpec},
util::{write_executable_file, write_executable_link},
};

/**
Expand Down Expand Up @@ -74,7 +76,7 @@ impl ToolStorage {
) -> AftmanResult<()> {
let (dir_path, file_path) = self.tool_paths(spec);
create_dir_all(dir_path).await?;
write_executable(&file_path, contents).await?;
write_executable_file(&file_path, contents).await?;
Ok(())
}

Expand All @@ -98,7 +100,7 @@ impl ToolStorage {
}
None => self.aftman_contents().await?,
};
write_executable(self.aftman_path(), &contents).await?;
write_executable_file(self.aftman_path(), &contents).await?;
Ok(())
}

Expand All @@ -110,17 +112,22 @@ impl ToolStorage {
pub async fn create_tool_link(&self, alias: &ToolAlias) -> AftmanResult<()> {
let path = self.aliases_dir.join(alias.name());
let contents = self.aftman_contents().await?;
write_executable(path, &contents).await?;
write_executable_file(path, &contents).await?;
Ok(())
}

/**
Recreates all known links for tool aliases in the binary directory.
This includes the link / main executable for Aftman itself.
This includes the link for Aftman itself - and if the link for Aftman does
not exist, `true` will be returned to indicate that the link was created.
Returns a tuple with information about any existing Aftman link:
- The first value is `true` if the existing Aftman link was found, `false` otherwise.
- The second value is `true` if the existing Aftman link was different compared to the
newly written Aftman binary, `false` otherwise. This is useful for determining if
the Aftman binary itself existed but was updated, such as during `self-install`.
*/
pub async fn recreate_all_links(&self) -> AftmanResult<bool> {
pub async fn recreate_all_links(&self) -> AftmanResult<(bool, bool)> {
let contents = self.aftman_contents().await?;
let aftman_path = self.aftman_path();
let mut aftman_found = false;
Expand All @@ -130,14 +137,17 @@ impl ToolStorage {
while let Some(entry) = link_reader.next_entry().await? {
let path = entry.path();
if path != aftman_path {
debug!(?path, "Found existing link");
link_paths.push(path);
} else {
aftman_found = true;
}
}

// Always write the Aftman binary to ensure it's up-to-date
write_executable(&aftman_path, &contents).await?;
let existing_aftman_binary = read(&aftman_path).await.unwrap_or_default();
let was_aftman_updated = existing_aftman_binary != contents;
write_executable_file(&aftman_path, &contents).await?;

// Then we can write the rest of the links - on unix we can use
// symlinks pointing to the aftman binary to save on disk space.
Expand All @@ -147,14 +157,14 @@ impl ToolStorage {
if cfg!(unix) {
write_executable_link(link_path, &aftman_path).await
} else {
write_executable(link_path, &contents).await
write_executable_file(link_path, &contents).await
}
})
.collect::<FuturesUnordered<_>>()
.try_collect::<Vec<_>>()
.await?;

Ok(!aftman_found)
Ok((aftman_found, was_aftman_updated))
}

pub(crate) async fn load(home_path: impl AsRef<Path>) -> AftmanResult<Self> {
Expand Down Expand Up @@ -182,46 +192,3 @@ impl ToolStorage {
})
}
}

async fn write_executable(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> AftmanResult<()> {
let path = path.as_ref();

use tokio::fs::write;
write(path, contents).await?;

#[cfg(unix)]
{
use std::fs::Permissions;
use std::os::unix::fs::PermissionsExt;
use tokio::fs::set_permissions;
set_permissions(path, Permissions::from_mode(0o755)).await?;
}

Ok(())
}

async fn write_executable_link(
link_path: impl AsRef<Path>,
target_path: impl AsRef<Path>,
) -> AftmanResult<()> {
let link_path = link_path.as_ref();
let target_path = target_path.as_ref();

#[cfg(unix)]
{
use tokio::fs::symlink;
symlink(target_path, link_path).await?;
}

// NOTE: We set the permissions of the symlink itself only on macOS
// since that is the only supported OS where symlink permissions matter
#[cfg(target_os = "macos")]
{
use std::fs::Permissions;
use std::os::unix::fs::PermissionsExt;
use tokio::fs::set_permissions;
set_permissions(link_path, Permissions::from_mode(0o755)).await?;
}

Ok(())
}
95 changes: 95 additions & 0 deletions lib/util.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::{convert::Infallible, path::Path, str::FromStr};

use tokio::fs::{read_to_string, write};
use tracing::error;

use crate::result::{AftmanError, AftmanResult};

Expand Down Expand Up @@ -63,3 +64,97 @@ where
write(path, data.to_string()).await?;
Ok(())
}

/**
Writes the given contents to the file at the
given path, and adds executable permissions to it.
*/
pub async fn write_executable_file(
path: impl AsRef<Path>,
contents: impl AsRef<[u8]>,
) -> AftmanResult<()> {
let path = path.as_ref();

if let Err(e) = write(path, contents).await {
error!("Failed to write executable to {path:?}:\n{e}");
return Err(e.into());
}

add_executable_permissions(path).await?;

Ok(())
}

/**
Writes a symlink at the given link path to the given
target path, and sets the symlink to be executable.
# Panics
This function will panic if called on a non-unix system.
*/
#[cfg(unix)]
pub async fn write_executable_link(
link_path: impl AsRef<Path>,
target_path: impl AsRef<Path>,
) -> AftmanResult<()> {
use tokio::fs::{remove_file, symlink};

let link_path = link_path.as_ref();
let target_path = target_path.as_ref();

// NOTE: If a symlink already exists, we may need to remove it
// for the new symlink to be created successfully - the only error we
// should be able to get here is if the file doesn't exist, which is fine.
remove_file(link_path).await.ok();

if let Err(e) = symlink(target_path, link_path).await {
error!("Failed to create symlink at {link_path:?}:\n{e}");
return Err(e.into());
}

// NOTE: We set the permissions of the symlink itself only on macOS
// since that is the only supported OS where symlink permissions matter
#[cfg(target_os = "macos")]
{
add_executable_permissions(link_path).await?;
}

Ok(())
}

/**
Writes a symlink at the given link path to the given
target path, and sets the symlink to be executable.
# Panics
This function will panic if called on a non-unix system.
*/
#[cfg(not(unix))]
pub async fn write_executable_link(
_link_path: impl AsRef<Path>,
_target_path: impl AsRef<Path>,
) -> AftmanResult<()> {
panic!("write_executable_link should only be called on unix systems");
}

#[cfg(unix)]
async fn add_executable_permissions(path: impl AsRef<Path>) -> AftmanResult<()> {
use std::fs::Permissions;
use std::os::unix::fs::PermissionsExt;
use tokio::fs::set_permissions;

let path = path.as_ref();
if let Err(e) = set_permissions(path, Permissions::from_mode(0o755)).await {
error!("Failed to set executable permissions on {path:?}:\n{e}");
return Err(e.into());
}

Ok(())
}

#[cfg(not(unix))]
async fn set_executable_permissions(_path: impl AsRef<Path>) -> AftmanResult<()> {
Ok(())
}
8 changes: 6 additions & 2 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ use aftman::storage::Home;
mod add;
mod install;
mod list;
mod self_install;
mod trust;

use self::add::AddSubcommand;
use self::install::InstallSubcommand;
use self::list::ListSubcommand;
use self::self_install::SelfInstallSubcommand;
use self::trust::TrustSubcommand;

#[derive(Debug, Parser)]
Expand Down Expand Up @@ -64,18 +66,20 @@ impl Cli {
#[derive(Debug, Parser)]
pub enum Subcommand {
Add(AddSubcommand),
Install(InstallSubcommand),
List(ListSubcommand),
SelfInstall(SelfInstallSubcommand),
Trust(TrustSubcommand),
Install(InstallSubcommand),
}

impl Subcommand {
pub async fn run(self, home: &Home) -> Result<()> {
match self {
Self::Add(cmd) => cmd.run(home).await,
Self::Install(cmd) => cmd.run(home).await,
Self::List(cmd) => cmd.run(home).await,
Self::SelfInstall(cmd) => cmd.run(home).await,
Self::Trust(cmd) => cmd.run(home).await,
Self::Install(cmd) => cmd.run(home).await,
}
}
}
41 changes: 41 additions & 0 deletions src/cli/self_install.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use anyhow::{Context, Result};
use clap::Parser;

use aftman::storage::Home;

/// Installs / re-installs Aftman, and updates all tool links.
#[derive(Debug, Parser)]
pub struct SelfInstallSubcommand {}

impl SelfInstallSubcommand {
pub async fn run(&self, home: &Home) -> Result<()> {
let storage = home.tool_storage();

let (had_aftman_installed, was_aftman_updated) =
storage.recreate_all_links().await.context(
"Failed to recreate tool links!\
\nYour installation may be corrupted.",
)?;

// TODO: Automatically populate the PATH variable
let path_was_populated = false;
let path_message_lines = if !path_was_populated {
"\nBinaries for Aftman and tools have been added to your PATH.\
\nPlease restart your terminal for the changes to take effect."
} else {
""
};

let main_message = if !had_aftman_installed {
"Aftman has been installed successfully!"
} else if was_aftman_updated {
"Aftman was re-linked successfully!"
} else {
"Aftman is already up-to-date."
};

tracing::info!("{main_message}{path_message_lines}");

Ok(())
}
}

0 comments on commit 1b315b7

Please sign in to comment.