Skip to content

Commit

Permalink
feat(run): run packages without installing them
Browse files Browse the repository at this point in the history
  • Loading branch information
QaidVoid committed Oct 10, 2024
1 parent 50d6b60 commit 16e820a
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 25 deletions.
12 changes: 10 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,16 @@ pub enum Commands {
},

/// Inspect package build log
#[command(name = "inspect")]
Inspect {
#[arg(required = true)]
package: String
}
package: String,
},

/// Run packages without installing to PATH
#[command(name = "run")]
Run {
#[arg(required = true)]
command: Vec<String>,
},
}
4 changes: 3 additions & 1 deletion src/core/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use anyhow::{Context, Result};
use futures::StreamExt;
use tokio::{
fs::{self, File},
io::AsyncReadExt,
io::{AsyncReadExt, AsyncWriteExt},
};

use super::constant::{BIN_PATH, INSTALL_TRACK_PATH};
Expand Down Expand Up @@ -106,6 +106,8 @@ pub async fn validate_checksum(checksum: &str, file_path: &Path) -> Result<()> {
hasher.update(&buffer[..n]);
}

file.flush().await?;

let final_checksum = hasher.finalize().to_hex().to_string();
if final_checksum == *checksum {
Ok(())
Expand Down
5 changes: 4 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ pub async fn init() -> Result<()> {
}
Commands::Inspect { package } => {
registry.inspect(&package).await?;
},
}
Commands::Run { command } => {
registry.run(command.as_ref()).await?;
}
};

Ok(())
Expand Down
4 changes: 4 additions & 0 deletions src/registry/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,10 @@ impl PackageRegistry {
pub async fn inspect(&self, package_name: &str) -> Result<()> {
self.storage.inspect(package_name).await
}

pub async fn run(&self, command: &[String]) -> Result<()> {
self.storage.run(command).await
}
}

pub fn select_package_variant(packages: &[ResolvedPackage]) -> Result<&ResolvedPackage> {
Expand Down
38 changes: 20 additions & 18 deletions src/registry/package/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@ impl Installer {
}

if is_installed && !is_update {
println!("{} Reinstalling package", prefix);
println!("{}: Reinstalling package", prefix);
}

if let Some(parent) = self.install_path.parent() {
fs::create_dir_all(parent).await.context(format!(
"{} Failed to create install directory {}",
"{}: Failed to create install directory {}",
prefix,
self.install_path.to_string_lossy()
))?;
Expand All @@ -77,7 +77,7 @@ impl Installer {
.header("Range", format!("bytes={}-", downloaded_bytes))
.send()
.await
.context(format!("{} Failed to download package", prefix))?;
.context(format!("{}: Failed to download package", prefix))?;
let total_size = response
.content_length()
.map(|cl| cl + downloaded_bytes)
Expand All @@ -90,26 +90,28 @@ impl Installer {

if !response.status().is_success() {
return Err(anyhow::anyhow!(
"{} Download failed: {:?}",
"{} Download failed with status code {:?}",
prefix,
response.status(),
));
}

let mut file = fs::OpenOptions::new()
.write(true)
.create(true)
.append(true)
.open(&temp_path)
.await
.context(format!("{} Failed to open temp file for writing", prefix))?;
let mut stream = response.bytes_stream();

while let Some(chunk) = stream.next().await {
let chunk = chunk.context(format!("{} Failed to read chunk", prefix))?;
file.write_all(&chunk).await?;
{
let mut file = fs::OpenOptions::new()
.write(true)
.create(true)
.append(true)
.open(&temp_path)
.await
.context(format!("{}: Failed to open temp file for writing", prefix))?;
let mut stream = response.bytes_stream();

while let Some(chunk) = stream.next().await {
let chunk = chunk.context(format!("{}: Failed to read chunk", prefix))?;
file.write_all(&chunk).await?;
}
file.flush().await?;
}
file.flush().await?;

if package.bsum == "null" {
eprintln!(
Expand Down Expand Up @@ -157,7 +159,7 @@ impl Installer {
}
tokio::fs::rename(&temp_path, &install_path).await?;
tokio::fs::set_permissions(&install_path, Permissions::from_mode(0o755)).await?;
xattr::set(install_path, "user.owner", b"soar")?;
xattr::set(install_path, "user.ManagedBy", b"soar")?;

Ok(())
}
Expand Down
15 changes: 14 additions & 1 deletion src/registry/package/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
mod install;
mod remove;
mod run;
pub mod update;

use std::{fmt::Display, path::PathBuf, sync::Arc};
use std::{
fmt::Display,
path::{Path, PathBuf},
sync::Arc,
};

use anyhow::Result;
use install::Installer;
use remove::Remover;
use run::Runner;
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;

Expand Down Expand Up @@ -63,6 +69,13 @@ impl ResolvedPackage {
remover.execute(&mut installed_packages).await?;
Ok(())
}

pub async fn run(&self, args: &[String], cache_dir: &Path) -> Result<()> {
let package_path = cache_dir.join(&self.package.bin_name);
let runner = Runner::new(self, package_path, args);
runner.execute().await?;
Ok(())
}
}

impl Package {
Expand Down
155 changes: 155 additions & 0 deletions src/registry/package/run.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
use std::{
fs::Permissions, io::Write, os::unix::fs::PermissionsExt, path::PathBuf, process::Command,
};

use anyhow::{Context, Result};
use futures::StreamExt;
use tokio::{fs, io::AsyncWriteExt};

use crate::core::util::{format_bytes, validate_checksum};

use super::ResolvedPackage;

pub struct Runner {
args: Vec<String>,
resolved_package: ResolvedPackage,
install_path: PathBuf,
temp_path: PathBuf,
}

impl Runner {
pub fn new(package: &ResolvedPackage, install_path: PathBuf, args: &[String]) -> Self {
let temp_path = install_path.with_extension("part");
Self {
args: args.to_owned(),
resolved_package: package.to_owned(),
install_path,
temp_path,
}
}

pub async fn execute(&self) -> Result<()> {
let package = &self.resolved_package.package;
let package_name = &package.full_name();

if self.install_path.exists() {
if xattr::get(&self.install_path, "user.ManagedBy")?.as_deref() != Some(b"soar") {
return Err(anyhow::anyhow!(
"Path {} is not managed by soar. Exiting.",
self.install_path.to_string_lossy()
));
} else {
println!("Found existing cache for {}", package_name);
let result = validate_checksum(&package.bsum, &self.install_path).await;
if result.is_err() {
eprintln!("Checksum validation failed for {}", package_name);
eprintln!("The package will be re-downloaded.");
} else {
self.run().await?;
return Ok(());
}
}
}

let client = reqwest::Client::new();
let downloaded_bytes = if self.temp_path.exists() {
let meta = fs::metadata(&self.temp_path).await?;
meta.len()
} else {
0
};

let response = client
.get(&package.download_url)
.header("Range", format!("bytes={}-", downloaded_bytes))
.send()
.await
.context(format!("{} Failed to download package", package_name))?;
let total_size = response
.content_length()
.map(|cl| cl + downloaded_bytes)
.unwrap_or(0);
println!(
"{}: Downloading package [{}]",
package_name,
format_bytes(total_size)
);

if !response.status().is_success() {
return Err(anyhow::anyhow!(
"{}: Download failed with status code {:?}",
package_name,
response.status()
));
}

{
let mut file = fs::OpenOptions::new()
.write(true)
.create(true)
.append(true)
.open(&self.temp_path)
.await
.context(format!(
"{}: Failed to open temp file for writing",
package_name
))?;

let mut stream = response.bytes_stream();

while let Some(chunk) = stream.next().await {
let chunk = chunk.context(format!("{}: Failed to read chunk", package_name))?;
file.write_all(&chunk).await?;
}
file.flush().await?;
}

if package.bsum == "null" {
eprintln!(
"Missing checksum for {}. Installing anyway.",
package.full_name()
);
} else {
let result = validate_checksum(&package.bsum, &self.temp_path).await;
if result.is_err() {
eprint!(
"\n{}: Checksum verification failed. Do you want to remove the package? (y/n): ",
package_name
);
std::io::stdout().flush()?;

let mut response = String::new();
std::io::stdin().read_line(&mut response)?;

if response.trim().eq_ignore_ascii_case("y") {
tokio::fs::remove_file(&self.temp_path).await?;
return Err(anyhow::anyhow!(""));
}
}
}

self.save_file().await?;
self.run().await?;

Ok(())
}

async fn save_file(&self) -> Result<()> {
let install_path = &self.install_path;
let temp_path = &self.temp_path;
if install_path.exists() {
tokio::fs::remove_file(&install_path).await?;
}
tokio::fs::rename(&temp_path, &install_path).await?;
tokio::fs::set_permissions(&install_path, Permissions::from_mode(0o755)).await?;
xattr::set(install_path, "user.ManagedBy", b"soar")?;

Ok(())
}

async fn run(&self) -> Result<()> {
Command::new(&self.install_path).args(&self.args).spawn()?;

Ok(())
}
}
36 changes: 34 additions & 2 deletions src/registry/storage.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::{
collections::HashMap,
env,
sync::{
atomic::{AtomicU64, Ordering},
Arc,
Expand All @@ -8,10 +9,16 @@ use std::{

use anyhow::Result;
use serde::{Deserialize, Serialize};
use tokio::sync::{Mutex, Semaphore};
use tokio::{
fs,
sync::{Mutex, Semaphore},
};

use crate::{
core::{config::CONFIG, util::download},
core::{
config::CONFIG,
util::{build_path, download},
},
registry::{
installed::InstalledPackages,
package::{parse_package_query, ResolvedPackage},
Expand Down Expand Up @@ -312,4 +319,29 @@ impl PackageStorage {

Ok(())
}

pub async fn run(&self, command: &[String]) -> Result<()> {
let mut cache_dir = env::var("XDG_CACHE_HOME").unwrap_or_else(|_| {
env::var("HOME").map_or_else(
|_| panic!("Failed to retrieve HOME environment variable"),
|home| format!("{}/.cache", home),
)
});
cache_dir.push_str("/soar");
let cache_dir = build_path(&cache_dir)?;

fs::create_dir_all(&cache_dir).await?;

let package_name = &command[0];
let resolved_pkg = self.resolve_package(package_name)?;

let args = if command.len() > 1 {
&command[1..]
} else {
&[]
};
resolved_pkg.run(args, &cache_dir).await?;

Ok(())
}
}

0 comments on commit 16e820a

Please sign in to comment.