Skip to content

Commit

Permalink
Implement running of tools, use symlinks where possible on unix to sa…
Browse files Browse the repository at this point in the history
…ve disk space
  • Loading branch information
filiptibell committed Mar 25, 2024
1 parent 159c495 commit ce54a4a
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 28 deletions.
99 changes: 76 additions & 23 deletions lib/storage/tool_storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ use std::{
sync::Arc,
};

use futures::{stream::FuturesUnordered, TryStreamExt};
use tokio::{
fs::{create_dir_all, read, read_dir, write},
fs::{create_dir_all, read, read_dir},
sync::Mutex as AsyncMutex,
task::{spawn_blocking, JoinSet},
task::spawn_blocking,
};

use crate::{
Expand Down Expand Up @@ -73,7 +74,7 @@ impl ToolStorage {
) -> AftmanResult<()> {
let (dir_path, file_path) = self.tool_paths(spec);
create_dir_all(dir_path).await?;
write(file_path, contents).await?;
write_executable(&file_path, contents).await?;
Ok(())
}

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

Expand All @@ -109,7 +110,7 @@ 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(&path, &contents).await?;
write_executable(path, &contents).await?;
Ok(())
}

Expand All @@ -121,30 +122,39 @@ impl ToolStorage {
*/
pub async fn recreate_all_links(&self) -> AftmanResult<bool> {
let contents = self.aftman_contents().await?;
let aftman_path = self.aftman_path();
let mut aftman_found = false;

let mut link_paths = Vec::new();
let mut link_reader = read_dir(&self.aliases_dir).await?;
while let Some(entry) = link_reader.next_entry().await? {
link_paths.push(entry.path());
}

let aftman_path = self.aftman_path();
let aftman_existed = if link_paths.contains(&aftman_path) {
true
} else {
link_paths.push(aftman_path);
false
};

let mut futures = JoinSet::new();
for link_path in link_paths {
futures.spawn(write(link_path, contents.clone()));
}
while let Some(result) = futures.join_next().await {
result??;
let path = entry.path();
if path != aftman_path {
link_paths.push(path);
} else {
aftman_found = true;
}
}

Ok(!aftman_existed)
// Always write the Aftman binary to ensure it's up-to-date
write_executable(&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.
link_paths
.into_iter()
.map(|link_path| async {
if cfg!(unix) {
write_executable_link(link_path, &aftman_path).await
} else {
write_executable(link_path, &contents).await
}
})
.collect::<FuturesUnordered<_>>()
.try_collect::<Vec<_>>()
.await?;

Ok(!aftman_found)
}

pub(crate) async fn load(home_path: impl AsRef<Path>) -> AftmanResult<Self> {
Expand Down Expand Up @@ -172,3 +182,46 @@ 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(())
}
42 changes: 40 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
use std::env::args;
use std::process::exit;
use std::str::FromStr;

use anyhow::{Context, Result};
use clap::Parser;
use tracing::{error, level_filters::LevelFilter};
use tracing_subscriber::EnvFilter;

use aftman::{storage::Home, system::run_interruptible, tool::ToolAlias};

mod cli;
mod util;
use cli::Cli;

use self::cli::Cli;
use self::util::{arg0_file_name, discover_closest_tool_spec};

#[cfg(debug_assertions)]
const FMT_PRETTY: bool = true;
Expand Down Expand Up @@ -43,11 +50,42 @@ async fn main() {
.init();
}

if let Err(e) = Cli::parse().run().await {
let exe_name = arg0_file_name();
let result = if exe_name == "aftman" {
run_cli().await
} else {
run_tool(exe_name).await
};

if let Err(e) = result {
// NOTE: We use tracing for errors here for consistent
// output between returned errors, and errors that
// may be logged while the program is running.
error!("{e:?}");
exit(1);
}
}

async fn run_cli() -> Result<()> {
Cli::parse().run().await
}

async fn run_tool(alias: String) -> Result<()> {
let alias = ToolAlias::from_str(&alias)?;

let home = Home::load_from_env().await?;

let result = async {
let spec = discover_closest_tool_spec(&home, &alias)
.await
.with_context(|| format!("Failed to find tool '{alias}'"))?;
let path = home.tool_storage().tool_path(&spec);
let args = args().skip(1).collect::<Vec<_>>();
anyhow::Ok(run_interruptible(&path, &args).await?)
}
.await;

home.save().await?;

exit(result?);
}
48 changes: 46 additions & 2 deletions src/util/discovery.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
use std::path::PathBuf;
use std::{env::current_dir, path::PathBuf};

use anyhow::{Context, Result};

use aftman::{
manifests::{discover_file_recursive, discover_files_recursive, AFTMAN_MANIFEST_FILE_NAME},
manifests::{
discover_file_recursive, discover_files_recursive, AftmanManifest,
AFTMAN_MANIFEST_FILE_NAME,
},
storage::Home,
tool::{ToolAlias, ToolSpec},
};
use futures::{stream::FuturesUnordered, TryStreamExt};
use tokio::task::spawn_blocking;

pub async fn discover_aftman_manifest_dir() -> Result<PathBuf> {
let file_path = discover_file_recursive(AFTMAN_MANIFEST_FILE_NAME)
Expand Down Expand Up @@ -33,3 +39,41 @@ pub async fn discover_aftman_manifest_dirs(home: &Home) -> Result<Vec<PathBuf>>

Ok(dirs)
}

pub async fn discover_closest_tool_spec(home: &Home, alias: &ToolAlias) -> Result<ToolSpec> {
let cwd = spawn_blocking(current_dir)
.await?
.context("Failed to get current working directory")?;

let dirs = discover_aftman_manifest_dirs(home).await?;
let manifests = dirs
.iter()
.map(|dir| async move {
let manifest = AftmanManifest::load(&dir)
.await
.with_context(|| format!("Failed to load manifest at {}", dir.display()))?;
anyhow::Ok((dir, manifest))
})
.collect::<FuturesUnordered<_>>()
.try_collect::<Vec<_>>()
.await?;

let specs = manifests
.iter()
.flat_map(|(dir, manifest)| {
let spec = manifest.get_tool(alias)?;
Some((*dir, spec))
})
.collect::<Vec<_>>();
let (_, closest_spec) = specs
.iter()
.min_by_key(|(dir, _)| {
dir.strip_prefix(&cwd)
.unwrap_or_else(|_| dir)
.components()
.count()
})
.context("No tool spec found for the given alias")?;

Ok(closest_spec.clone())
}
6 changes: 5 additions & 1 deletion src/util/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
mod discovery;
mod id_or_spec;
mod prompts;
mod running;
mod sources;

pub use self::discovery::{discover_aftman_manifest_dir, discover_aftman_manifest_dirs};
pub use self::discovery::{
discover_aftman_manifest_dir, discover_aftman_manifest_dirs, discover_closest_tool_spec,
};
pub use self::id_or_spec::ToolIdOrSpec;
pub use self::prompts::prompt_for_install_trust;
pub use self::running::arg0_file_name;
pub use self::sources::github_tool_source;
16 changes: 16 additions & 0 deletions src/util/running.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use std::{
env::{args, consts::EXE_EXTENSION},
path::PathBuf,
};

pub fn arg0_file_name() -> String {
let arg0 = args().next().unwrap();
let exe_path = PathBuf::from(arg0);
let exe_name = exe_path
.file_name()
.expect("Invalid file name passed as arg0")
.to_str()
.expect("Non-UTF8 file name passed as arg0")
.trim_end_matches(EXE_EXTENSION);
exe_name.to_string()
}

0 comments on commit ce54a4a

Please sign in to comment.