Skip to content
This repository was archived by the owner on May 18, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions check.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
$ErrorActionPreference = "Stop"

cargo fmt
cargo clippy --all-features --all-targets
cargo test
50 changes: 45 additions & 5 deletions lib/src/actions/package/providers/winget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ use crate::contexts::Contexts;
use crate::steps::Step;
use crate::{actions::package::PackageVariant, atoms::command::Exec};
use serde::{Deserialize, Serialize};
use tracing::warn;
use std::process::Command;
use tracing::{debug, trace, warn};
use which::which;

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
Expand Down Expand Up @@ -42,15 +43,54 @@ impl PackageProvider for Winget {
}

fn query(&self, package: &PackageVariant) -> anyhow::Result<Vec<String>> {
// Install all packages, make this smarter soon
Ok(package.packages())
// Find all packages that aren't already installed
Ok(package
.packages()
.into_iter()
.filter(|p| {
// We use `winget list -e --id <package_name>`
// `--accept-source-agreements` prevents it from blocking on first run
let output = Command::new("winget")
.args([
"list",
"-e",
"--id",
p.as_str(),
"--accept-source-agreements",
])
.output();

match output {
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout);
// Winget returns 0 even if nothing is found, so we must parse the output.
// If it contains the package ID, it's installed. Otherwise, it prints "No installed package found"
if stdout.to_lowercase().contains(&p.to_lowercase()) {
trace!("{}: already installed", p);
false // installed, so filter it out
} else {
debug!("{}: doesn't appear to be installed", p);
true // not installed, keep it
}
}
Err(e) => {
warn!("Failed to query winget for package {}: {}", p, e);
true // assume not installed on error to attempt install
}
}
})
.collect())
}

fn install(&self, package: &PackageVariant, _contexts: &Contexts) -> anyhow::Result<Vec<Step>> {
// does not require privilege escalation

Ok(package
.packages()
let need_installed = self.query(package)?;
if need_installed.is_empty() {
return Ok(vec![]);
}

Ok(need_installed
.iter()
.map::<Step, _>(|p| Step {
atom: Box::new(Exec {
Expand Down
2 changes: 1 addition & 1 deletion lib/src/actions/user/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ impl Action for UserAdd {
}

#[cfg(not(unix))]
atoms.append(&mut provider.add_user(&variant, &context)?);
atoms.append(&mut provider.add_user(&variant, context)?);

Ok(atoms)
}
Expand Down
22 changes: 18 additions & 4 deletions lib/src/atoms/file/link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ impl Atom for Link {

#[cfg(windows)]
fn execute(&mut self) -> anyhow::Result<()> {
if self.target.is_dir() {
if self.source.is_dir() {
std::os::windows::fs::symlink_dir(&self.source, &self.target)?;
} else {
std::os::windows::fs::symlink_file(&self.source, &self.target)?;
Expand Down Expand Up @@ -128,8 +128,22 @@ mod tests {
target: from_dir.path().join("symlink"),
source: to_file.path().to_path_buf(),
};
assert_eq!(true, atom.plan().unwrap().should_run);
assert_eq!(true, atom.execute().is_ok());
assert_eq!(false, atom.plan().unwrap().should_run);
match atom.execute() {
Ok(_) => {
assert_eq!(false, atom.plan().unwrap().should_run);
}
Err(e) => {
#[cfg(windows)]
if let Some(os_err) = e.downcast_ref::<std::io::Error>() {
if os_err.raw_os_error() == Some(1314) {
// "A required privilege is not held by the client"
// Symlinks on Windows require Developer Mode or Admin rights
println!("Skipping link test execution due to missing privileges (OS Error 1314).");
return;
}
}
panic!("Execute failed: {}", e);
}
}
}
}