Skip to content
This repository has been archived by the owner on Oct 15, 2022. It is now read-only.

Commit

Permalink
lorri shell
Browse files Browse the repository at this point in the history
  • Loading branch information
curiousleo committed Jan 23, 2020
1 parent 3ee6ae1 commit df02150
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 10 deletions.
6 changes: 6 additions & 0 deletions release.nix
Expand Up @@ -5,6 +5,12 @@
changelog = {
# Find the current version number with `git log --pretty=%h | wc -l`
entries = [
{
version = 429;
changes = ''
- Re-introduced `lorri shell` command.
'';
}
{
version = 309;
changes = ''
Expand Down
6 changes: 0 additions & 6 deletions shell.nix
Expand Up @@ -40,7 +40,6 @@ let
pkgs.cargo
pkgs.rustc
pkgs.rustfmt
pkgs.bashInteractive
pkgs.git
pkgs.direnv
pkgs.shellcheck
Expand Down Expand Up @@ -92,11 +91,6 @@ pkgs.mkShell (
exec 3>&1 # store stdout (1) in fd 3
exec 1>&2 # make stdout (1) an alias for stderr (2)
# this is needed so `lorri shell` runs the proper shell from
# inside this project's nix-shell. If you run `lorri` within a
# nix-shell, you don't need this.
export SHELL="${pkgs.bashInteractive}/bin/bash";
alias ci="ci_check"
# this is mirrored from .envrc to make available from nix-shell
Expand Down
18 changes: 15 additions & 3 deletions src/cli.rs
Expand Up @@ -29,6 +29,10 @@ pub enum Command {
#[structopt(name = "info", alias = "information")]
Info(InfoOptions),

/// Open a new shell
#[structopt(name = "shell")]
Shell(ShellOptions),

/// Build `shell.nix` whenever an input file changes
#[structopt(name = "watch")]
Watch(WatchOptions),
Expand All @@ -50,15 +54,15 @@ pub enum Command {
Init,
}

/// Options for `watch` subcommand.
/// Options for the `direnv` subcommand.
#[derive(StructOpt, Debug)]
pub struct DirenvOptions {
/// The .nix file in the current directory to use
#[structopt(long = "shell-file", parse(from_os_str), default_value = "shell.nix")]
pub nix_file: PathBuf,
}

/// Options for `watch` subcommand.
/// Options for the `info` subcommand.
#[derive(StructOpt, Debug)]
pub struct InfoOptions {
/// The .nix file in the current directory to use
Expand All @@ -69,7 +73,15 @@ pub struct InfoOptions {
pub nix_file: PathBuf,
}

/// Options for `watch` subcommand.
/// Options for the `shell` subcommand.
#[derive(StructOpt, Debug)]
pub struct ShellOptions {
/// The .nix file in the current directory to use
#[structopt(long = "shell-file", parse(from_os_str), default_value = "shell.nix")]
pub nix_file: PathBuf,
}

/// Options for the `watch` subcommand.
#[derive(StructOpt, Debug)]
pub struct WatchOptions {
/// The .nix file in the current directory to use
Expand Down
6 changes: 5 additions & 1 deletion src/main.rs
Expand Up @@ -3,7 +3,7 @@ use lorri::constants;
use lorri::locate_file;
use lorri::logging;
use lorri::ops::error::{ExitError, OpResult};
use lorri::ops::{daemon, direnv, info, init, ping, upgrade, watch};
use lorri::ops::{daemon, direnv, info, init, ping, shell, upgrade, watch};
use lorri::project::Project;
use lorri::NixFile;
use slog::{debug, error, o};
Expand Down Expand Up @@ -90,6 +90,10 @@ fn run_command(log: slog::Logger, opts: Arguments) -> OpResult {
let (project, _guard) = with_project(&opts.nix_file)?;
direnv::main(project, /* shell_output */ std::io::stdout())
}
Command::Shell(opts) => {
let (project, _guard) = with_project(&opts.nix_file)?;
shell::main(project)
}
Command::Watch(opts) => {
let (project, _guard) = with_project(&opts.nix_file)?;
watch::main(project, opts)
Expand Down
1 change: 1 addition & 0 deletions src/ops/mod.rs
Expand Up @@ -5,6 +5,7 @@ pub mod direnv;
pub mod info;
pub mod init;
pub mod ping;
pub mod shell;
pub mod upgrade;
pub mod watch;

Expand Down
87 changes: 87 additions & 0 deletions src/ops/shell.rs
@@ -0,0 +1,87 @@
//! Open up a project shell

use crate::builder;
use crate::builder::RunStatus;
use crate::nix::CallOpts;
use crate::ops::error::{ExitError, OpResult};
use crate::project::{roots::Roots, Project};
use crossbeam_channel as chan;
use slog_scope::debug;
use std::io;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{Duration, Instant};
use std::{env, fs, thread};

/// See the documentation for lorri::cli::Command::Shell for more
/// details.
pub fn main(project: Project) -> OpResult {
let shell = env::var("SHELL").expect("lorri shell requires $SHELL to be set");
debug!("using shell path {}", shell);

let tempdir = tempfile::tempdir().expect("failed to create temporary directory");
let mut bash_cmd = bash_cmd(project, tempdir.path())?;
debug!("bash"; "command" => ?bash_cmd);
bash_cmd
.args(&["-c", &format!("exec {}", shell)])
.status()
.expect("failed to execute bash");
Ok(())
}

/// Instantiates a `Command` to start bash.
pub fn bash_cmd(project: Project, tempdir: &Path) -> Result<Command, ExitError> {
let (tx, rx) = chan::unbounded();
thread::spawn(move || {
eprint!("lorri: building environment");
let mut last = Instant::now();
for msg in rx {
// Set the maximum rate of the "progress bar"
if last.elapsed() >= Duration::from_millis(500) {
eprint!(".");
io::stderr().flush().unwrap();
last = Instant::now();
}
debug!("build"; "message" => ?msg);
}
eprintln!(". done");
});

let run_result = builder::run(tx, &project.nix_file, &project.cas)
.map_err(|e| ExitError::temporary(format!("build failed: {:?}", e)))?;
let build = match run_result.status {
RunStatus::Complete(build) => Roots::from_project(&project)
.create_roots(build)
.map_err(|e| ExitError::temporary(format!("rooting the environment failed: {:?}", e))),
e => Err(ExitError::temporary(format!("build failed: {:?}", e))),
}?;

let init_file = tempdir.join("init");
fs::write(
&init_file,
format!(
r#"
EVALUATION_ROOT="{}"
{}"#,
build.shell_gc_root,
include_str!("direnv/envrc.bash")
),
)
.expect("failed to write shell output");

debug!("building bash via runtime closure"; "closure" => crate::RUN_TIME_CLOSURE);
let bash_path = CallOpts::expression(&format!("(import {}).path", crate::RUN_TIME_CLOSURE))
.value::<PathBuf>()
.expect("failed to get runtime closure path");

let mut cmd = Command::new(bash_path.join("bash"));
cmd.env(
"BASH_ENV",
init_file
.to_str()
.expect("script file path not UTF-8 clean"),
);
Ok(cmd)
}
6 changes: 6 additions & 0 deletions tests/shell/loads_env/shell.nix
@@ -0,0 +1,6 @@
with import ../../../nix/bogus-nixpkgs {};
mkShell {
env = {
MY_ENV_VAR = "my_env_value";
};
}
34 changes: 34 additions & 0 deletions tests/shell/main.rs
@@ -0,0 +1,34 @@
use lorri::{cas::ContentAddressable, ops::shell, project::Project, NixFile};
use std::fs;
use std::iter::FromIterator;
use std::path::{Path, PathBuf};

#[test]
fn loads_env() {
let tempdir = tempfile::tempdir().expect("tempfile::tempdir() failed us!");
let project = project("loads_env", tempdir.path());
let output = shell::bash_cmd(project, tempdir.path())
.unwrap()
.args(&["-c", "echo $MY_ENV_VAR"])
.output()
.expect("failed to run shell");

assert_eq!(
// The string conversion means we get a nice assertion failure message in case stdout does
// not match what we expected.
String::from_utf8(output.stdout).expect("stdout not UTF-8 clean"),
"my_env_value\n"
);
}

fn project(name: &str, cache_dir: &Path) -> Project {
let test_root = PathBuf::from_iter(&[env!("CARGO_MANIFEST_DIR"), "tests", "shell", name]);
let cas_dir = cache_dir.join("cas").to_owned();
fs::create_dir_all(&cas_dir).expect("failed to create CAS directory");
Project::new(
NixFile::Shell(test_root.join("shell.nix")),
&cache_dir.join("gc_roots").to_owned(),
ContentAddressable::new(cas_dir).unwrap(),
)
.unwrap()
}

0 comments on commit df02150

Please sign in to comment.