Skip to content

Commit c594d13

Browse files
committed
refactor and README rewrite
1 parent 425c4d0 commit c594d13

File tree

8 files changed

+642
-839
lines changed

8 files changed

+642
-839
lines changed

Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ license = "Apache-2.0"
1010
name = "cargox"
1111
path = "src/main.rs"
1212

13+
[package.metadata.binstall]
14+
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-{ version }-{ target }.tar.gz"
15+
pkg-fmt = "tgz"
16+
bin-dir = "{ bin }{ binary-ext }"
17+
18+
[package.metadata.binstall.overrides.x86_64-pc-windows-msvc]
19+
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-{ version }-{ target }.zip"
20+
pkg-fmt = "zip"
21+
1322
[dependencies]
1423
anyhow = "1.0"
1524
clap = { version = "4.5", features = ["derive"] }

README.md

Lines changed: 8 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,16 @@ cargox bat ./README.md
3030
cargox cargo-deny@0.16.3 check
3131

3232
# Force a reinstall using cargo install instead of cargo-binstall
33-
cargox cargo-nextest --force --use-cargo-install
33+
cargox --force --use-cargo-install cargo-nextest
3434
```
3535

36+
> [!TIP]
37+
>
38+
> - Arguments before the first positional are passed to `cargox`.
39+
> - Arguments after `--` are passed to the invoked binary.
40+
> - Use `--` if necessary to define the separation point.
41+
> - If the crate exposes multiple binaries, use `--bin <name>` to select one.
42+
3643
### Flags
3744

3845
- `--bin <name>`: choose a specific binary when a crate exposes several.
@@ -90,36 +97,3 @@ This sandboxing guarantees that:
9097
that is automatically cleaned up after installation completes
9198

9299
This keeps your system clean and prevents build cache bloat.
93-
94-
**Adding binaries to your PATH:**
95-
96-
To use binaries installed by `cargox`, ensure the install directory is in your `PATH`:
97-
98-
```bash
99-
# Linux/Unix - add to ~/.bashrc, ~/.zshrc, or equivalent
100-
export PATH="$HOME/.local/share/cargox/bin:$PATH"
101-
102-
# macOS - add to ~/.zshrc or ~/.bash_profile
103-
export PATH="$HOME/Library/Application Support/cargox/bin:$PATH"
104-
105-
# Or use the custom directory if you set CARGOX_INSTALL_DIR
106-
export PATH="/your/custom/path/bin:$PATH"
107-
```
108-
109-
## Development
110-
111-
```bash
112-
cargo fmt
113-
cargo test
114-
```
115-
116-
`cargox` (published as `cargox-cli`) is distributed as a standard Cargo binary
117-
crate; build it locally with `cargo build --release` if you want a standalone
118-
executable.
119-
120-
## Releasing
121-
122-
Tag releases with `vX.Y.Z` and push the tag. A GitHub Actions workflow (powered
123-
by `cargo-dist`) builds binaries for Linux, macOS (Intel + Apple Silicon), and
124-
Windows, uploads them as release assets, and generates the metadata that
125-
`cargo-binstall` needs to install `cargox` directly from those releases.

src/cli.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
use clap::Parser;
2+
use std::ffi::OsString;
3+
4+
/// Run Cargo binaries on demand, installing them via `cargo-binstall` when missing.
5+
#[derive(Parser, Debug)]
6+
#[command(author, version, about, long_about = None, arg_required_else_help = true)]
7+
pub struct Cli {
8+
/// Crate to run, optionally suffixed with `@version`
9+
#[arg(value_name = "crate[@version]")]
10+
pub crate_spec: String,
11+
12+
/// Execute this binary from the crate (defaults to crate name)
13+
#[arg(long, value_name = "NAME")]
14+
pub bin: Option<String>,
15+
16+
/// Force reinstall even if the binary is already on PATH
17+
#[arg(short, long)]
18+
pub force: bool,
19+
20+
/// Suppress installer output
21+
#[arg(short, long)]
22+
pub quiet: bool,
23+
24+
/// Use `cargo install` instead of `cargo-binstall`
25+
#[arg(long)]
26+
pub use_cargo_install: bool,
27+
28+
/// Arguments passed to the executed binary (use `--` to delimit)
29+
#[arg(trailing_var_arg = true, value_name = "binary-args")]
30+
pub args: Vec<OsString>,
31+
}

src/executor.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
use anyhow::{Context, Result};
2+
use std::ffi::OsString;
3+
use std::path::Path;
4+
use std::process::{Command, ExitStatus};
5+
6+
pub fn execute_binary(binary_path: &Path, args: &[OsString]) -> Result<ExitStatus> {
7+
let mut cmd = Command::new(binary_path);
8+
cmd.args(args);
9+
10+
let status = cmd
11+
.status()
12+
.with_context(|| format!("failed to execute {}", binary_path.display()))?;
13+
14+
Ok(status)
15+
}

src/installer.rs

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
use anyhow::{anyhow, Context, Result};
2+
use std::process::Command;
3+
4+
use crate::cli::Cli;
5+
use crate::paths::get_install_dir;
6+
use crate::target::Target;
7+
8+
pub fn ensure_installed(target: &Target, cli: &Cli) -> Result<()> {
9+
if !cli.use_cargo_install && which::which("cargo-binstall").is_ok() {
10+
install_with_binstall(target, cli)
11+
} else {
12+
log_fallback_reason(cli, target);
13+
install_with_cargo(target, cli)
14+
}
15+
}
16+
17+
fn log_fallback_reason(cli: &Cli, target: &Target) {
18+
if cli.use_cargo_install {
19+
eprintln!(
20+
"cargo-binstall was skipped explicitly; installing {} with cargo install",
21+
target.descriptor()
22+
);
23+
} else {
24+
eprintln!(
25+
"cargo-binstall not found; falling back to cargo install for {}",
26+
target.descriptor()
27+
);
28+
}
29+
}
30+
31+
fn install_with_binstall(target: &Target, cli: &Cli) -> Result<()> {
32+
let install_dir = get_install_dir()?;
33+
34+
let mut cmd = Command::new("cargo");
35+
cmd.arg("binstall");
36+
if cli.quiet {
37+
cmd.arg("--quiet");
38+
}
39+
cmd.arg("--no-confirm");
40+
if cli.force {
41+
cmd.arg("--force");
42+
}
43+
if let Some(bin) = &cli.bin {
44+
cmd.arg("--bin");
45+
cmd.arg(bin);
46+
}
47+
cmd.arg(target.install_spec());
48+
49+
// Set the install root for cargo-binstall and remove any environment variables
50+
// that could leak into the installation process
51+
sanitize_cargo_env(&mut cmd, &install_dir);
52+
53+
eprintln!(
54+
"Installing {} with cargo-binstall{} to {}",
55+
target.descriptor(),
56+
if cli.quiet { " (quiet)" } else { "" },
57+
install_dir.display()
58+
);
59+
60+
let status = cmd.status().context("failed to invoke cargo-binstall")?;
61+
if status.success() {
62+
Ok(())
63+
} else {
64+
Err(anyhow!(
65+
"cargo-binstall exited with status code {}",
66+
status
67+
.code()
68+
.map(|c| c.to_string())
69+
.unwrap_or_else(|| "signal".to_string())
70+
))
71+
}
72+
}
73+
74+
fn install_with_cargo(target: &Target, cli: &Cli) -> Result<()> {
75+
let install_dir = get_install_dir()?;
76+
77+
// Create a temporary directory for the build
78+
let temp_dir = tempfile::tempdir().context("failed to create temp directory")?;
79+
80+
let mut cmd = Command::new("cargo");
81+
cmd.arg("install");
82+
if cli.quiet {
83+
cmd.arg("--quiet");
84+
}
85+
if cli.force {
86+
cmd.arg("--force");
87+
}
88+
cmd.arg("--root");
89+
cmd.arg(&install_dir);
90+
cmd.arg(&target.crate_name);
91+
if let Some(version) = &target.version {
92+
cmd.arg("--version");
93+
cmd.arg(version);
94+
}
95+
if let Some(bin) = &cli.bin {
96+
cmd.arg("--bin");
97+
cmd.arg(bin);
98+
}
99+
100+
// Use temp directory for target build directory and sanitize environment
101+
cmd.env("CARGO_TARGET_DIR", temp_dir.path());
102+
sanitize_cargo_env(&mut cmd, &install_dir);
103+
104+
eprintln!(
105+
"Installing {} with cargo install{} to {}",
106+
target.descriptor(),
107+
if cli.quiet { " (quiet)" } else { "" },
108+
install_dir.display()
109+
);
110+
111+
let status = cmd.status().context("failed to invoke cargo install")?;
112+
113+
// Temp directory will be automatically cleaned up when temp_dir goes out of scope
114+
115+
if status.success() {
116+
Ok(())
117+
} else {
118+
Err(anyhow!(
119+
"cargo install exited with status code {}",
120+
status
121+
.code()
122+
.map(|c| c.to_string())
123+
.unwrap_or_else(|| "signal".to_string())
124+
))
125+
}
126+
}
127+
128+
/// Sanitize the environment for cargo commands to ensure complete sandboxing.
129+
/// Removes any Cargo-related environment variables that could leak into the installation
130+
/// and sets only the variables we explicitly want.
131+
fn sanitize_cargo_env(cmd: &mut Command, install_dir: &std::path::Path) {
132+
// List of environment variables to remove to ensure sandboxing
133+
let vars_to_remove = [
134+
"CARGO_INSTALL_ROOT",
135+
"CARGO_HOME",
136+
"CARGO_BUILD_TARGET_DIR",
137+
"CARGO_TARGET_DIR",
138+
"BINSTALL_INSTALL_PATH",
139+
"RUSTUP_HOME",
140+
"RUSTUP_TOOLCHAIN",
141+
];
142+
143+
for var in &vars_to_remove {
144+
cmd.env_remove(var);
145+
}
146+
147+
// Set only our controlled install location
148+
cmd.env("CARGO_INSTALL_ROOT", install_dir);
149+
}
150+
151+
#[cfg(test)]
152+
mod tests {
153+
use super::*;
154+
155+
#[test]
156+
fn sanitize_cargo_env_removes_cargo_variables() {
157+
let temp = tempfile::tempdir().unwrap();
158+
let install_dir = temp.path();
159+
160+
// Create a command with cargo env vars set
161+
let mut cmd = Command::new("echo");
162+
cmd.env("CARGO_INSTALL_ROOT", "/some/path");
163+
cmd.env("CARGO_HOME", "/some/cargo");
164+
cmd.env("BINSTALL_INSTALL_PATH", "/some/binstall");
165+
cmd.env("RUSTUP_HOME", "/some/rustup");
166+
cmd.env("CARGO_TARGET_DIR", "/some/target");
167+
cmd.env("SOME_OTHER_VAR", "should_remain");
168+
169+
// Sanitize the environment
170+
sanitize_cargo_env(&mut cmd, install_dir);
171+
172+
// Note: We can't directly inspect Command's env, but we can verify
173+
// the function exists and compiles correctly. The actual behavior
174+
// is tested through integration tests.
175+
}
176+
}

0 commit comments

Comments
 (0)