Skip to content

Commit

Permalink
quickenv now warns to run 'shim' after 'pip install'
Browse files Browse the repository at this point in the history
See #1
  • Loading branch information
untitaker committed Aug 12, 2022
1 parent 4990847 commit 4b12ff6
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 42 deletions.
10 changes: 0 additions & 10 deletions src/confirm.rs

This file was deleted.

118 changes: 91 additions & 27 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ use anyhow::{Context, Error};
use clap::Parser;
use console::style;

mod confirm;
mod core;
mod grid;
mod signals;

use crate::core::resolve_envrc_context;

Expand All @@ -30,6 +30,8 @@ use crate::core::resolve_envrc_context;
after_help = "ENVIRONMENT VARIABLES:
QUICKENV_LOG=debug to enable debug output (in shim commands as well)
QUICKENV_LOG=error to silence everything but errors
QUICKENV_NO_SHIM=1 to disable loading of .envrc, and effectively disable shims
QUICKENV_SHIM_EXEC=1 to directly exec() shims instead of spawning them as subprocess. This can help with attaching debuggers.
"
)]
struct Args {
Expand Down Expand Up @@ -138,7 +140,7 @@ fn main_inner() -> Result<(), Error> {

let args = Args::parse();

crate::confirm::set_ctrlc_handler()?;
crate::signals::set_ctrlc_handler()?;

match args.subcommand {
Command::Reload => command_reload(),
Expand Down Expand Up @@ -311,6 +313,8 @@ echo '// END QUICKENV-AFTER'
)
})?;

signals::pass_control_to_shim();

let mut cmd = process::Command::new("bash")
.arg(temp_script.path())
.env("QUICKENV_NO_SHIM", "1")
Expand Down Expand Up @@ -357,8 +361,8 @@ echo '// END QUICKENV-AFTER'
fn get_missing_shims(
quickenv_home: &Path,
new_path_envvar: Option<&OsStr>,
) -> Result<Vec<String>, Error> {
let mut rv = Vec::new();
) -> Result<BTreeSet<String>, Error> {
let mut rv = BTreeSet::new();
let new_path_envvar = match new_path_envvar {
Some(x) => x,
None => return Ok(rv),
Expand Down Expand Up @@ -390,7 +394,7 @@ fn get_missing_shims(
fn get_missing_shims_from_dir(
quickenv_home: &Path,
path: &Path,
rv: &mut Vec<String>,
rv: &mut BTreeSet<String>,
) -> Result<(), Error> {
for entry in std::fs::read_dir(path)? {
let entry = entry?;
Expand All @@ -413,7 +417,7 @@ fn get_missing_shims_from_dir(
};

if !quickenv_home.join("bin").join(filename).exists() {
rv.push(filename.to_owned());
rv.insert(filename.to_owned());
}
}

Expand All @@ -423,21 +427,60 @@ fn get_missing_shims_from_dir(
fn command_reload() -> Result<(), Error> {
let quickenv_home = crate::core::get_quickenv_home()?;
compute_envvars(&quickenv_home)?;
let ctx = resolve_envrc_context(&quickenv_home)?;
let new_envvars =
crate::core::get_envvars(&ctx)?.expect("somehow didn't end up writing envvars");
let new_path_envvar = new_envvars.get(OsStr::new("PATH")).map(OsString::as_os_str);
let missing_shims = get_missing_shims(&quickenv_home, new_path_envvar)?;
CheckUnshimmedCommands::new(&quickenv_home)?.check_unshimmed_commands()?;

if !missing_shims.is_empty() {
log::info!(
"{} unshimmed commands. Use {} to make them available.",
style(missing_shims.len()).green(),
style("'quickenv shim'").magenta(),
)
Ok(())
}

struct CheckUnshimmedCommands<'a> {
ctx: core::EnvrcContext,
quickenv_home: &'a Path,
old_missing_shims: BTreeSet<String>,
}

impl<'a> CheckUnshimmedCommands<'a> {
fn new(quickenv_home: &'a Path) -> Result<Self, Error> {
Ok(CheckUnshimmedCommands {
ctx: resolve_envrc_context(quickenv_home)?,
quickenv_home,
old_missing_shims: BTreeSet::new(),
})
}

Ok(())
fn exclude_current(&mut self) -> Result<(), Error> {
let envvars = match crate::core::get_envvars(&self.ctx)? {
Some(x) => x,
None => return Ok(()),
};

let new_path_envvar = envvars.get(OsStr::new("PATH")).map(OsString::as_os_str);

self.old_missing_shims = get_missing_shims(self.quickenv_home, new_path_envvar)?;
Ok(())
}

fn check_unshimmed_commands(self) -> Result<(), Error> {
let envvars = match crate::core::get_envvars(&self.ctx)? {
Some(x) => x,
None => return Ok(()),
};

let new_path_envvar = envvars.get(OsStr::new("PATH")).map(OsString::as_os_str);
let mut missing_shims = get_missing_shims(self.quickenv_home, new_path_envvar)?;
for elem in &self.old_missing_shims {
missing_shims.remove(elem);
}

if !missing_shims.is_empty() {
log::warn!(
"{} unshimmed commands. Use {} to make them available.",
style(missing_shims.len()).green(),
style("'quickenv shim'").magenta(),
)
}

Ok(())
}
}

fn command_vars() -> Result<(), Error> {
Expand Down Expand Up @@ -481,7 +524,9 @@ fn command_shim(mut commands: Vec<String>, yes: bool) -> Result<(), Error> {
}
};
let path_envvar = envvars.get(OsStr::new("PATH")).map(OsString::as_os_str);
commands = get_missing_shims(&quickenv_home, path_envvar)?;
commands = get_missing_shims(&quickenv_home, path_envvar)?
.into_iter()
.collect();

if !commands.is_empty() {
eprintln!(
Expand Down Expand Up @@ -628,17 +673,36 @@ fn exec_shimmed_binary(program_name: &OsStr, args: Vec<OsString>) -> Result<(),
let shimmed_binary_result = find_shimmed_binary(&quickenv_home, program_name)
.context("failed to find actual binary")?;

for (k, v) in shimmed_binary_result.envvars_override {
log::debug!("export {:?}={:?}", k, v);
std::env::set_var(k, v);
}
if std::env::var("QUICKENV_SHIM_EXEC").unwrap_or_default() == "1" {
for (k, v) in shimmed_binary_result.envvars_override {
log::debug!("export {:?}={:?}", k, v);
std::env::set_var(k, v);
}

log::debug!("execvp {}", shimmed_binary_result.path.display());

let mut full_args = vec![shimmed_binary_result.path.clone().into_os_string()];
full_args.extend(args);

Err(exec::execvp(&shimmed_binary_result.path, &full_args).into())
} else {
let mut unshimmed_commands = CheckUnshimmedCommands::new(&quickenv_home);
let _ignored = unshimmed_commands.as_mut().map(|x| x.exclude_current());

log::debug!("execvp {}", shimmed_binary_result.path.display());
let exitcode = process::Command::new(shimmed_binary_result.path)
.args(args)
.envs(shimmed_binary_result.envvars_override)
.status()
.context("failed to spawn shim subcommand")?;

let mut full_args = vec![shimmed_binary_result.path.clone().into_os_string()];
full_args.extend(args);
let _ignored = unshimmed_commands.map(|x| x.check_unshimmed_commands());

Err(exec::execvp(&shimmed_binary_result.path, &full_args).into())
if let Some(code) = exitcode.code() {
std::process::exit(code);
}

Ok(())
}
}

struct ShimmedBinaryResult {
Expand Down
25 changes: 25 additions & 0 deletions src/signals.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use anyhow::Error;
use std::process::exit;

use std::sync::atomic::{AtomicBool, Ordering};

static SHIM_HAS_CONTROL: AtomicBool = AtomicBool::new(false);
const INTERRUPTED_EXIT_CODE: i32 = 130;

pub fn pass_control_to_shim() {
// the control-passing behavior was blatantly stolen from volta.
// https://github.com/volta-cli/volta/blob/5b5c94285500b1023f773215a7ef85aaeeeaffbd/crates/volta-core/src/signal.rs
SHIM_HAS_CONTROL.store(true, Ordering::SeqCst);
}

pub fn set_ctrlc_handler() -> Result<(), Error> {
ctrlc::set_handler(move || {
if !SHIM_HAS_CONTROL.load(Ordering::SeqCst) {
// necessary to work around https://github.com/mitsuhiko/dialoguer/issues/188
let term = console::Term::stdout();
term.show_cursor().unwrap();
exit(INTERRUPTED_EXIT_CODE);
}
})?;
Ok(())
}
95 changes: 90 additions & 5 deletions tests/acceptance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ fn test_basic() -> Result<(), Error> {
----- stdout -----
----- stderr -----
1 unshimmed commands. Use 'quickenv shim' to make them available.
[WARN quickenv] 1 unshimmed commands. Use 'quickenv shim' to make them available.
"###);
harness.which("hello").unwrap_err();
assert_cmd!(harness, quickenv "shim" "hello", @r###"
Expand Down Expand Up @@ -58,7 +58,7 @@ fn test_basic() -> Result<(), Error> {
----- stdout -----
----- stderr -----
1 unshimmed commands. Use 'quickenv shim' to make them available.
[WARN quickenv] 1 unshimmed commands. Use 'quickenv shim' to make them available.
"###);
Ok(())
}
Expand Down Expand Up @@ -278,7 +278,7 @@ fn test_exec() -> Result<(), Error> {
----- stdout -----
----- stderr -----
1 unshimmed commands. Use 'quickenv shim' to make them available.
[WARN quickenv] 1 unshimmed commands. Use 'quickenv shim' to make them available.
"###);

harness.which("hello").unwrap_err();
Expand All @@ -293,6 +293,91 @@ fn test_exec() -> Result<(), Error> {
Ok(())
}

#[test]
fn test_shim_creating_shims() -> Result<(), Error> {
let harness = setup()?;

write(harness.join(".envrc"), "export PATH=bogus:$PATH\n")?;
assert_cmd!(harness, quickenv "reload", @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
"###);

create_dir_all(harness.join("bogus"))?;
write(harness.join("bogus/hello"), "#!/bin/sh\necho hello world")?;
set_executable(harness.join("bogus/hello"))?;

// there is a command. it does not create more commands. quickenv should not amend any output
assert_cmd!(harness, quickenv "exec" "hello", @r###"
success: true
exit_code: 0
----- stdout -----
hello world
----- stderr -----
"###);

// shimming the command should work
assert_cmd!(harness, quickenv "shim" "--yes", @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Found these unshimmed commands in your .envrc:
hello
Quickenv will create this new shim binary in [scrubbed $HOME]/.quickenv/bin/.
Inside of [scrubbed $HOME]/project, those commands will run with .envrc enabled.
Outside, they will run normally.
Created 1 new shims in [scrubbed $HOME]/.quickenv/bin/.
Use 'quickenv unshim <command>' to remove them again.
Use 'quickenv shim <command>' to run additional commands with .envrc enabled.
"###);

// change the command such that it creates another command, and run it
write(
harness.join("bogus/hello"),
"#!/bin/sh\necho 'echo hello world' > bogus/hello2 && chmod +x bogus/hello2",
)?;
set_executable(harness.join("bogus/hello"))?;

// quickenv should warn that more commands need shimming now
assert_cmd!(harness, quickenv "exec" "hello", @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
[WARN quickenv] 1 unshimmed commands. Use 'quickenv shim' to make them available.
"###);

// quickenv shim should find the new command
assert_cmd!(harness, quickenv "shim" "--yes", @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Found these unshimmed commands in your .envrc:
hello2
Quickenv will create this new shim binary in [scrubbed $HOME]/.quickenv/bin/.
Inside of [scrubbed $HOME]/project, those commands will run with .envrc enabled.
Outside, they will run normally.
Created 1 new shims in [scrubbed $HOME]/.quickenv/bin/.
Use 'quickenv unshim <command>' to remove them again.
Use 'quickenv shim <command>' to run additional commands with .envrc enabled.
"###);

Ok(())
}

#[test]
fn test_auto_shimming() -> Result<(), Error> {
let harness = setup()?;
Expand All @@ -308,7 +393,7 @@ fn test_auto_shimming() -> Result<(), Error> {
----- stdout -----
----- stderr -----
1 unshimmed commands. Use 'quickenv shim' to make them available.
[WARN quickenv] 1 unshimmed commands. Use 'quickenv shim' to make them available.
"###);

assert_cmd!(harness, quickenv "shim" "-y", @r###"
Expand Down Expand Up @@ -427,7 +512,7 @@ fn test_which() -> Result<(), Error> {
----- stdout -----
----- stderr -----
1 unshimmed commands. Use 'quickenv shim' to make them available.
[WARN quickenv] 1 unshimmed commands. Use 'quickenv shim' to make them available.
"###);
assert_cmd!(harness, quickenv "which" "hello", @r###"
success: false
Expand Down

0 comments on commit 4b12ff6

Please sign in to comment.