397 lines · 12.6 KB · Rust
RawDownload
#![cfg_attr(not(unix), allow(unused))] use anyhow::{Context as _, Result};use std::borrow::Cow; /// Capture all environment variables from the login shell.#[cfg(unix)]pub fn capture(directory: &std::path::Path) -> Result<collections::HashMap<String, String>> {    use std::os::unix::process::CommandExt;    use std::process::Stdio;     let shell_path = std::env::var("SHELL").map(std::path::PathBuf::from)?;    let shell_name = shell_path.file_name().and_then(std::ffi::OsStr::to_str);     let mut command = std::process::Command::new(&shell_path);    command.stdin(Stdio::null());    command.stdout(Stdio::piped());    command.stderr(Stdio::piped());     let mut command_string = String::new();     // What we're doing here is to spawn a shell and then `cd` into    // the project directory to get the env in there as if the user    // `cd`'d into it. We do that because tools like direnv, asdf, ...    // hook into `cd` and only set up the env after that.    command_string.push_str(&format!("cd '{}';", directory.display()));     // In certain shells we need to execute additional_command in order to    // trigger the behavior of direnv, etc.    command_string.push_str(match shell_name {        Some("fish") => "emit fish_prompt;",        _ => "",    });     // In some shells, file descriptors greater than 2 cannot be used in interactive mode,    // so file descriptor 0 is used instead.    const ENV_OUTPUT_FD: std::os::fd::RawFd = 0;    command_string.push_str(&format!("sh -c 'export -p >&{ENV_OUTPUT_FD}';"));     // For csh/tcsh, the login shell option is set by passing `-` as    // the 0th argument instead of using `-l`.    if let Some("tcsh" | "csh") = shell_name {        command.arg0("-");    } else {        command.arg("-l");    }     command.args(["-i", "-c", &command_string]);     super::set_pre_exec_to_start_new_session(&mut command);     let (env_output, process_output) = spawn_and_read_fd(command, ENV_OUTPUT_FD)?;    let env_output = String::from_utf8_lossy(&env_output);     anyhow::ensure!(        process_output.status.success(),        "login shell exited with {}. stdout: {:?}, stderr: {:?}",        process_output.status,        String::from_utf8_lossy(&process_output.stdout),        String::from_utf8_lossy(&process_output.stderr),    );     parse(&env_output)        .filter_map(|entry| match entry {            Ok((name, value)) => Some(Ok((name.into(), value?.into()))),            Err(err) => Some(Err(err)),        })        .collect::<Result<_>>()} #[cfg(unix)]fn spawn_and_read_fd(    mut command: std::process::Command,    child_fd: std::os::fd::RawFd,) -> anyhow::Result<(Vec<u8>, std::process::Output)> {    use command_fds::{CommandFdExt, FdMapping};    use std::io::Read;     let (mut reader, writer) = std::io::pipe()?;     command.fd_mappings(vec![FdMapping {        parent_fd: writer.into(),        child_fd,    }])?;     let process = command.spawn()?;    drop(command);     let mut buffer = Vec::new();    reader.read_to_end(&mut buffer)?;     Ok((buffer, process.wait_with_output()?))} /// Parse the result of calling `sh -c 'export -p'`.////// https://www.man7.org/linux/man-pages/man1/export.1p.htmlfn parse(mut input: &str) -> impl Iterator<Item = Result<(Cow<'_, str>, Option<Cow<'_, str>>)>> {    std::iter::from_fn(move || {        if input.is_empty() {            return None;        }        match parse_declaration(input) {            Ok((entry, rest)) => {                input = rest;                Some(Ok(entry))            }            Err(err) => Some(Err(err)),        }    })} fn parse_declaration(input: &str) -> Result<((Cow<'_, str>, Option<Cow<'_, str>>), &str)> {    let rest = input        .strip_prefix("export ")        .context("expected 'export ' prefix")?;     if let Some((name, rest)) = parse_name_and_terminator(rest, '\n') {        Ok(((name, None), rest))    } else {        let (name, rest) = parse_name_and_terminator(rest, '=').context("invalid name")?;        let (value, rest) = parse_literal_and_terminator(rest, '\n').context("invalid value")?;        Ok(((name, Some(value)), rest))    }} fn parse_name_and_terminator(input: &str, terminator: char) -> Option<(Cow<'_, str>, &str)> {    let (name, rest) = parse_literal_and_terminator(input, terminator)?;    (!name.is_empty() && !name.contains('=')).then_some((name, rest))} fn parse_literal_and_terminator(input: &str, terminator: char) -> Option<(Cow<'_, str>, &str)> {    if let Some((literal, rest)) = parse_literal_ansi_c_quoted(input) {        let rest = rest.strip_prefix(terminator)?;        Some((Cow::Owned(literal), rest))    } else if let Some((literal, rest)) = parse_literal_single_quoted(input) {        let rest = rest.strip_prefix(terminator)?;        Some((Cow::Borrowed(literal), rest))    } else if let Some((literal, rest)) = parse_literal_double_quoted(input) {        let rest = rest.strip_prefix(terminator)?;        Some((Cow::Owned(literal), rest))    } else {        let (literal, rest) = input.split_once(terminator)?;        (!literal.contains(|c: char| c.is_ascii_whitespace()))            .then_some((Cow::Borrowed(literal), rest))    }} /// https://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.htmlfn parse_literal_ansi_c_quoted(input: &str) -> Option<(String, &str)> {    let rest = input.strip_prefix("$'")?;     let mut char_indices = rest.char_indices();    let mut escaping = false;    let (literal, rest) = loop {        let (index, char) = char_indices.next()?;        if char == '\'' && !escaping {            break (&rest[..index], &rest[index + 1..]);        } else {            escaping = !escaping && char == '\\';        }    };     let mut result = String::new();    let mut chars = literal.chars();    while let Some(ch) = chars.next() {        if ch == '\\' {            match chars.next() {                Some('n') => result.push('\n'),                Some('t') => result.push('\t'),                Some('r') => result.push('\r'),                Some('\\') => result.push('\\'),                Some('\'') => result.push('\''),                Some('"') => result.push('"'),                Some('a') => result.push('\x07'), // bell                Some('b') => result.push('\x08'), // backspace                Some('f') => result.push('\x0C'), // form feed                Some('v') => result.push('\x0B'), // vertical tab                Some('0') => result.push('\0'),   // null                Some(other) => {                    // For unknown escape sequences, keep the backslash and character                    result.push('\\');                    result.push(other);                }                None => result.push('\\'), // trailing backslash            }        } else {            result.push(ch);        }    }     Some((result, rest))} /// https://www.gnu.org/software/bash/manual/html_node/Single-Quotes.htmlfn parse_literal_single_quoted(input: &str) -> Option<(&str, &str)> {    input.strip_prefix('\'')?.split_once('\'')} /// https://www.gnu.org/software/bash/manual/html_node/Double-Quotes.htmlfn parse_literal_double_quoted(input: &str) -> Option<(String, &str)> {    let rest = input.strip_prefix('"')?;     let mut char_indices = rest.char_indices();    let mut escaping = false;    let (literal, rest) = loop {        let (index, char) = char_indices.next()?;        if char == '"' && !escaping {            break (&rest[..index], &rest[index + 1..]);        } else {            escaping = !escaping && char == '\\';        }    };     let literal = literal        .replace("\\$", "$")        .replace("\\`", "`")        .replace("\\\"", "\"")        .replace("\\\n", "")        .replace("\\\\", "\\");     Some((literal, rest))} #[cfg(test)]mod tests {    use super::*;     #[cfg(unix)]    #[test]    fn test_spawn_and_read_fd() -> anyhow::Result<()> {        let mut command = std::process::Command::new("sh");        super::super::set_pre_exec_to_start_new_session(&mut command);        command.args(["-lic", "printf 'abc%.0s' $(seq 1 65536) >&0"]);        let (bytes, _) = spawn_and_read_fd(command, 0)?;        assert_eq!(bytes.len(), 65536 * 3);        Ok(())    }     #[test]    fn test_parse() {        let input = indoc::indoc! {r#"        export foo        export 'foo'        export "foo"        export foo=        export 'foo'=        export "foo"=        export foo=bar        export foo='bar'        export foo="bar"        export foo='b        a        z'        export foo="b        a        z"        export foo='b\        a\        z'        export foo="b\        a\        z"        export foo='\`Hello\`        \"wo\        rld\"\n!\\        !'        export foo="\`Hello\`        \"wo\        rld\"\n!\\        !"        export foo=$'hello\nworld'        "#};         let expected_values = [            None,            None,            None,            Some(""),            Some(""),            Some(""),            Some("bar"),            Some("bar"),            Some("bar"),            Some("b\na\nz"),            Some("b\na\nz"),            Some("b\\\na\\\nz"),            Some("baz"),            Some(indoc::indoc! {r#"            \`Hello\`            \"wo\            rld\"\n!\\            !"#}),            Some(indoc::indoc! {r#"            `Hello`            "world"\n!\!"#}),            Some("hello\nworld"),        ];        let expected = expected_values            .into_iter()            .map(|value| ("foo".into(), value.map(Into::into)))            .collect::<Vec<_>>();         let actual = parse(input).collect::<Result<Vec<_>>>().unwrap();        assert_eq!(expected, actual);    }     #[test]    fn test_parse_declaration() {        let ((name, value), rest) = parse_declaration("export foo\nrest").unwrap();        assert_eq!(name, "foo");        assert_eq!(value, None);        assert_eq!(rest, "rest");         let ((name, value), rest) = parse_declaration("export foo=bar\nrest").unwrap();        assert_eq!(name, "foo");        assert_eq!(value.as_deref(), Some("bar"));        assert_eq!(rest, "rest");    }     #[test]    fn test_parse_literal_single_quoted() {        let input = indoc::indoc! {r#"        '\`Hello\`        \"wo\        rld\"\n!\\        !'        rest"#};         let expected = indoc::indoc! {r#"        \`Hello\`        \"wo\        rld\"\n!\\        !"#};         let (actual, rest) = parse_literal_single_quoted(input).unwrap();        assert_eq!(expected, actual);        assert_eq!(rest, "\nrest");    }     #[test]    fn test_parse_literal_double_quoted() {        let input = indoc::indoc! {r#"        "\`Hello\`        \"wo\        rld\"\n!\\        !"        rest"#};         let expected = indoc::indoc! {r#"        `Hello`        "world"\n!\!"#};         let (actual, rest) = parse_literal_double_quoted(input).unwrap();        assert_eq!(expected, actual);        assert_eq!(rest, "\nrest");    }     #[test]    fn test_parse_literal_ansi_c_quoted() {        let (actual, rest) = parse_literal_ansi_c_quoted("$'hello\\nworld'\nrest").unwrap();        assert_eq!(actual, "hello\nworld");        assert_eq!(rest, "\nrest");         let (actual, rest) = parse_literal_ansi_c_quoted("$'tab\\there'\nrest").unwrap();        assert_eq!(actual, "tab\there");        assert_eq!(rest, "\nrest");         let (actual, rest) = parse_literal_ansi_c_quoted("$'quote\\'\\'end'\nrest").unwrap();        assert_eq!(actual, "quote''end");        assert_eq!(rest, "\nrest");         let (actual, rest) = parse_literal_ansi_c_quoted("$'backslash\\\\end'\nrest").unwrap();        assert_eq!(actual, "backslash\\end");        assert_eq!(rest, "\nrest");    }     #[test]    fn test_parse_buildphase_export() {        let input = r#"export buildPhase=$'{ echo "------------------------------------------------------------";\n  echo " WARNING: the existence of this path is not guaranteed.";\n  echo " It is an internal implementation detail for pkgs.mkShell.";\n  echo "------------------------------------------------------------";\n  echo;\n  # Record all build inputs as runtime dependencies\n  export;\n} >> "$out"\n'"#;         let expected_value = r#"{ echo "------------------------------------------------------------";  echo " WARNING: the existence of this path is not guaranteed.";  echo " It is an internal implementation detail for pkgs.mkShell.";  echo "------------------------------------------------------------";  echo;  # Record all build inputs as runtime dependencies  export;} >> "$out""#;         let ((name, value), _rest) = parse_declaration(input).unwrap();        assert_eq!(name, "buildPhase");        assert_eq!(value.as_deref(), Some(expected_value));    }}