process_wrapper: replace C++ implementation with rust. (#1159)

diff --git a/rust/private/rustc.bzl b/rust/private/rustc.bzl
index 61e1b46..596c10d 100644
--- a/rust/private/rustc.bzl
+++ b/rust/private/rustc.bzl
@@ -922,20 +922,41 @@
             dsym_folder = ctx.actions.declare_directory(crate_info.output.basename + ".dSYM")
             action_outputs.append(dsym_folder)
 
-    ctx.actions.run(
-        executable = ctx.executable._process_wrapper,
-        inputs = compile_inputs,
-        outputs = action_outputs,
-        env = env,
-        arguments = args.all,
-        mnemonic = "Rustc",
-        progress_message = "Compiling Rust {} {}{} ({} files)".format(
-            crate_info.type,
-            ctx.label.name,
-            formatted_version,
-            len(crate_info.srcs.to_list()),
-        ),
-    )
+    # This uses startswith as on windows the basename will be process_wrapper_fake.exe.
+    if not ctx.executable._process_wrapper.basename.startswith("process_wrapper_fake"):
+        # Run as normal
+        ctx.actions.run(
+            executable = ctx.executable._process_wrapper,
+            inputs = compile_inputs,
+            outputs = action_outputs,
+            env = env,
+            arguments = args.all,
+            mnemonic = "Rustc",
+            progress_message = "Compiling Rust {} {}{} ({} files)".format(
+                crate_info.type,
+                ctx.label.name,
+                formatted_version,
+                len(crate_info.srcs.to_list()),
+            ),
+        )
+    else:
+        # Run without process_wrapper
+        if build_env_files or build_flags_files or stamp:
+            fail("build_env_files, build_flags_files, stamp are not supported if use_process_wrapper is False")
+        ctx.actions.run(
+            executable = toolchain.rustc,
+            inputs = compile_inputs,
+            outputs = action_outputs,
+            env = env,
+            arguments = [args.rustc_flags],
+            mnemonic = "Rustc",
+            progress_message = "Compiling Rust (without process_wrapper) {} {}{} ({} files)".format(
+                crate_info.type,
+                ctx.label.name,
+                formatted_version,
+                len(crate_info.srcs.to_list()),
+            ),
+        )
 
     runfiles = ctx.runfiles(
         files = getattr(ctx.files, "data", []),
diff --git a/rust/private/transitions.bzl b/rust/private/transitions.bzl
index f502163..76fbb78 100644
--- a/rust/private/transitions.bzl
+++ b/rust/private/transitions.bzl
@@ -64,3 +64,48 @@
         ),
     },
 )
+
+def _without_process_wrapper_transition_impl(_settings, _attr):
+    """This transition allows rust_* rules to invoke rustc without process_wrapper."""
+    return {
+        "//rust/settings:use_process_wrapper": False,
+    }
+
+without_process_wrapper_transition = transition(
+    implementation = _without_process_wrapper_transition_impl,
+    inputs = [],
+    outputs = ["//rust/settings:use_process_wrapper"],
+)
+
+def _without_process_wrapper_impl(ctx):
+    executable = ctx.executable.target
+    link_name = ctx.label.name
+
+    # Append .exe if on windows
+    if executable.extension:
+        link_name = link_name + "." + executable.extension
+    link = ctx.actions.declare_file(link_name)
+    ctx.actions.symlink(
+        output = link,
+        target_file = executable,
+    )
+    return [
+        DefaultInfo(
+            executable = link,
+        ),
+    ]
+
+without_process_wrapper = rule(
+    implementation = _without_process_wrapper_impl,
+    attrs = {
+        "target": attr.label(
+            cfg = without_process_wrapper_transition,
+            allow_single_file = True,
+            mandatory = True,
+            executable = True,
+        ),
+        "_allowlist_function_transition": attr.label(
+            default = Label("@bazel_tools//tools/allowlists/function_transition_allowlist"),
+        ),
+    },
+)
diff --git a/rust/settings/BUILD.bazel b/rust/settings/BUILD.bazel
index 8be7aab..474237a 100644
--- a/rust/settings/BUILD.bazel
+++ b/rust/settings/BUILD.bazel
@@ -29,6 +29,13 @@
     build_setting_default = False,
 )
 
+# A flag that enables/disables the use of process_wrapper when invoking rustc.
+# This is useful for building process_wrapper itself without causing dependency cycles.
+bool_flag(
+    name = "use_process_wrapper",
+    build_setting_default = True,
+)
+
 bzl_library(
     name = "bzl_lib",
     srcs = glob(["**/*.bzl"]),
diff --git a/util/process_wrapper/BUILD.bazel b/util/process_wrapper/BUILD.bazel
index 4b60036..c26d9a6 100644
--- a/util/process_wrapper/BUILD.bazel
+++ b/util/process_wrapper/BUILD.bazel
@@ -1,26 +1,45 @@
 load("@rules_cc//cc:defs.bzl", "cc_binary")
+load("//rust:defs.bzl", "rust_binary", "rust_test")
 
-cc_binary(
+# buildifier: disable=bzl-visibility
+load("//rust/private:transitions.bzl", "without_process_wrapper")
+
+alias(
     name = "process_wrapper",
-    srcs = [
-        "process_wrapper.cc",
-        "system.h",
-        "utils.h",
-        "utils.cc",
-    ] + select({
-        "@bazel_tools//src/conditions:host_windows": [
-            "system_windows.cc",
-        ],
-        "//conditions:default": [
-            "system_posix.cc",
-        ],
-    }),
-    defines = [] + select({
-        "@bazel_tools//src/conditions:host_windows": [
-            "UNICODE",
-            "_UNICODE",
-        ],
-        "//conditions:default": [],
+    actual = select({
+        # This will never get used, it's only here to break the circular dependency to allow building process_wrapper
+        ":use_fake_process_wrapper": ":process_wrapper_fake",
+        "//conditions:default": ":process_wrapper_impl",
     }),
     visibility = ["//visibility:public"],
 )
+
+cc_binary(
+    name = "process_wrapper_fake",
+    srcs = ["fake.cc"],
+)
+
+config_setting(
+    name = "use_fake_process_wrapper",
+    flag_values = {
+        "//rust/settings:use_process_wrapper": "False",
+    },
+)
+
+# Changing the name of this rule requires a corresponding
+# change in //rust/private/rustc.bzl:925
+without_process_wrapper(
+    name = "process_wrapper_impl",
+    target = ":process_wrapper_bin",
+    visibility = ["//visibility:public"],
+)
+
+rust_binary(
+    name = "process_wrapper_bin",
+    srcs = glob(["*.rs"]),
+)
+
+rust_test(
+    name = "process_wrapper_test",
+    crate = ":process_wrapper_bin",
+)
diff --git a/util/process_wrapper/fake.cc b/util/process_wrapper/fake.cc
new file mode 100644
index 0000000..a0b6869
--- /dev/null
+++ b/util/process_wrapper/fake.cc
@@ -0,0 +1,4 @@
+int main() {
+    // Noop on purpose.
+    return 0;
+}
\ No newline at end of file
diff --git a/util/process_wrapper/flags.rs b/util/process_wrapper/flags.rs
new file mode 100644
index 0000000..d3d6fe5
--- /dev/null
+++ b/util/process_wrapper/flags.rs
@@ -0,0 +1,276 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use std::collections::{BTreeMap, HashSet};
+use std::error::Error;
+use std::fmt;
+use std::fmt::Write;
+use std::iter::Peekable;
+use std::mem::take;
+
+#[derive(Debug, Clone)]
+pub(crate) enum FlagParseError {
+    UnknownFlag(String),
+    ValueMissing(String),
+    ProvidedMultipleTimes(String),
+    ProgramNameMissing,
+}
+
+impl fmt::Display for FlagParseError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::UnknownFlag(ref flag) => write!(f, "unknown flag \"{}\"", flag),
+            Self::ValueMissing(ref flag) => write!(f, "flag \"{}\" missing parameter(s)", flag),
+            Self::ProvidedMultipleTimes(ref flag) => {
+                write!(f, "flag \"{}\" can only appear once", flag)
+            }
+            Self::ProgramNameMissing => {
+                write!(f, "program name (argv[0]) missing")
+            }
+        }
+    }
+}
+impl Error for FlagParseError {}
+
+struct FlagDef<'a, T> {
+    name: String,
+    help: String,
+    output_storage: &'a mut Option<T>,
+}
+
+impl<'a, T> fmt::Display for FlagDef<'a, T> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{}\t{}", self.name, self.help)
+    }
+}
+
+impl<'a, T> fmt::Debug for FlagDef<'a, T> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.debug_struct("FlagDef")
+            .field("name", &self.name)
+            .field("help", &self.help)
+            .finish()
+    }
+}
+
+#[derive(Debug)]
+pub(crate) struct Flags<'a> {
+    single: BTreeMap<String, FlagDef<'a, String>>,
+    repeated: BTreeMap<String, FlagDef<'a, Vec<String>>>,
+}
+
+#[derive(Debug)]
+pub(crate) enum ParseOutcome {
+    Help(String),
+    Parsed(Vec<String>),
+}
+
+impl<'a> Flags<'a> {
+    pub(crate) fn new() -> Flags<'a> {
+        Flags {
+            single: BTreeMap::new(),
+            repeated: BTreeMap::new(),
+        }
+    }
+
+    pub(crate) fn define_flag(
+        &mut self,
+        name: impl Into<String>,
+        help: impl Into<String>,
+        output_storage: &'a mut Option<String>,
+    ) {
+        let name = name.into();
+        if self.repeated.contains_key(&name) {
+            panic!("argument \"{}\" already defined as repeated flag", name)
+        }
+        self.single.insert(
+            name.clone(),
+            FlagDef::<'a, String> {
+                name,
+                help: help.into(),
+                output_storage,
+            },
+        );
+    }
+
+    pub(crate) fn define_repeated_flag(
+        &mut self,
+        name: impl Into<String>,
+        help: impl Into<String>,
+        output_storage: &'a mut Option<Vec<String>>,
+    ) {
+        let name = name.into();
+        if self.single.contains_key(&name) {
+            panic!("argument \"{}\" already defined as flag", name)
+        }
+        self.repeated.insert(
+            name.clone(),
+            FlagDef::<'a, Vec<String>> {
+                name,
+                help: help.into(),
+                output_storage,
+            },
+        );
+    }
+
+    fn help(&self, program_name: String) -> String {
+        let single = self.single.values().map(|fd| fd.to_string());
+        let repeated = self.repeated.values().map(|fd| fd.to_string());
+        let mut all: Vec<String> = single.chain(repeated).collect();
+        all.sort();
+
+        let mut help_text = String::new();
+        writeln!(
+            &mut help_text,
+            "Help for {}: [options] -- [extra arguments]",
+            program_name
+        )
+        .unwrap();
+        for line in all {
+            writeln!(&mut help_text, "\t{}", line).unwrap();
+        }
+        help_text
+    }
+
+    pub(crate) fn parse(mut self, argv: Vec<String>) -> Result<ParseOutcome, FlagParseError> {
+        let mut argv_iter = argv.into_iter().peekable();
+        let program_name = argv_iter.next().ok_or(FlagParseError::ProgramNameMissing)?;
+
+        // To check if a non-repeated flag has been set already.
+        let mut seen_single_flags = HashSet::<String>::new();
+
+        while let Some(flag) = argv_iter.next() {
+            if flag == "--help" {
+                return Ok(ParseOutcome::Help(self.help(program_name)));
+            }
+            if !flag.starts_with("--") {
+                return Err(FlagParseError::UnknownFlag(flag));
+            }
+            let mut args = consume_args(&flag, &mut argv_iter);
+            if flag == "--" {
+                return Ok(ParseOutcome::Parsed(args));
+            }
+            if args.is_empty() {
+                return Err(FlagParseError::ValueMissing(flag.clone()));
+            }
+            if let Some(flag_def) = self.single.get_mut(&flag) {
+                if args.len() > 1 || seen_single_flags.contains(&flag) {
+                    return Err(FlagParseError::ProvidedMultipleTimes(flag.clone()));
+                }
+                let arg = args.first_mut().unwrap();
+                seen_single_flags.insert(flag);
+                *flag_def.output_storage = Some(take(arg));
+                continue;
+            }
+            if let Some(flag_def) = self.repeated.get_mut(&flag) {
+                flag_def
+                    .output_storage
+                    .get_or_insert_with(Vec::new)
+                    .append(&mut args);
+                continue;
+            }
+            return Err(FlagParseError::UnknownFlag(flag));
+        }
+        Ok(ParseOutcome::Parsed(vec![]))
+    }
+}
+
+fn consume_args<I: Iterator<Item = String>>(
+    flag: &str,
+    argv_iter: &mut Peekable<I>,
+) -> Vec<String> {
+    if flag == "--" {
+        // If we have found --, the rest of the iterator is just returned as-is.
+        argv_iter.collect()
+    } else {
+        let mut args = vec![];
+        while let Some(arg) = argv_iter.next_if(|s| !s.starts_with("--")) {
+            args.push(arg);
+        }
+        args
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    fn args(args: &[&str]) -> Vec<String> {
+        ["foo"].iter().chain(args).map(|&s| s.to_owned()).collect()
+    }
+
+    #[test]
+    fn test_flag_help() {
+        let mut bar = None;
+        let mut parser = Flags::new();
+        parser.define_flag("--bar", "bar help", &mut bar);
+        let result = parser.parse(args(&["--help"])).unwrap();
+        if let ParseOutcome::Help(h) = result {
+            assert!(h.contains("Help for foo"));
+            assert!(h.contains("--bar\tbar help"));
+        } else {
+            panic!("expected that --help would invoke help, instead parsed arguments")
+        }
+    }
+
+    #[test]
+    fn test_flag_single_repeated() {
+        let mut bar = None;
+        let mut parser = Flags::new();
+        parser.define_flag("--bar", "bar help", &mut bar);
+        let result = parser.parse(args(&["--bar", "aa", "bb"]));
+        if let Err(FlagParseError::ProvidedMultipleTimes(f)) = result {
+            assert_eq!(f, "--bar");
+        } else {
+            panic!("expected error, got {:?}", result)
+        }
+        let mut parser = Flags::new();
+        parser.define_flag("--bar", "bar help", &mut bar);
+        let result = parser.parse(args(&["--bar", "aa", "--bar", "bb"]));
+        if let Err(FlagParseError::ProvidedMultipleTimes(f)) = result {
+            assert_eq!(f, "--bar");
+        } else {
+            panic!("expected error, got {:?}", result)
+        }
+    }
+
+    #[test]
+    fn test_repeated_flags() {
+        // Test case 1) --bar something something_else should work as a repeated flag.
+        let mut bar = None;
+        let mut parser = Flags::new();
+        parser.define_repeated_flag("--bar", "bar help", &mut bar);
+        let result = parser.parse(args(&["--bar", "aa", "bb"])).unwrap();
+        assert!(matches!(result, ParseOutcome::Parsed(_)));
+        assert_eq!(bar, Some(vec!["aa".to_owned(), "bb".to_owned()]));
+        // Test case 2) --bar something --bar something_else should also work as a repeated flag.
+        bar = None;
+        let mut parser = Flags::new();
+        parser.define_repeated_flag("--bar", "bar help", &mut bar);
+        let result = parser.parse(args(&["--bar", "aa", "--bar", "bb"])).unwrap();
+        assert!(matches!(result, ParseOutcome::Parsed(_)));
+        assert_eq!(bar, Some(vec!["aa".to_owned(), "bb".to_owned()]));
+    }
+
+    #[test]
+    fn test_extra_args() {
+        let parser = Flags::new();
+        let result = parser.parse(args(&["--", "bb"])).unwrap();
+        if let ParseOutcome::Parsed(got) = result {
+            assert_eq!(got, vec!["bb".to_owned()])
+        } else {
+            panic!("expected correct parsing, got {:?}", result)
+        }
+    }
+}
diff --git a/util/process_wrapper/main.rs b/util/process_wrapper/main.rs
new file mode 100644
index 0000000..41140a3
--- /dev/null
+++ b/util/process_wrapper/main.rs
@@ -0,0 +1,79 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+mod flags;
+mod options;
+mod util;
+
+use std::fs::{copy, OpenOptions};
+use std::process::{exit, Command, Stdio};
+
+use crate::options::options;
+
+fn main() {
+    let opts = match options() {
+        Err(err) => panic!("process wrapper error: {}", err),
+        Ok(v) => v,
+    };
+    let stdout = if let Some(stdout_file) = opts.stdout_file {
+        OpenOptions::new()
+            .create(true)
+            .truncate(true)
+            .write(true)
+            .open(stdout_file)
+            .expect("process wrapper error: unable to open stdout file")
+            .into()
+    } else {
+        Stdio::inherit()
+    };
+    let stderr = if let Some(stderr_file) = opts.stderr_file {
+        OpenOptions::new()
+            .create(true)
+            .truncate(true)
+            .write(true)
+            .open(stderr_file)
+            .expect("process wrapper error: unable to open stderr file")
+            .into()
+    } else {
+        Stdio::inherit()
+    };
+    let status = Command::new(opts.executable)
+        .args(opts.child_arguments)
+        .env_clear()
+        .envs(opts.child_environment)
+        .stdout(stdout)
+        .stderr(stderr)
+        .status()
+        .expect("process wrapper error: failed to spawn child process");
+
+    if status.success() {
+        if let Some(tf) = opts.touch_file {
+            OpenOptions::new()
+                .create(true)
+                .write(true)
+                .open(tf)
+                .expect("process wrapper error: failed to create touch file");
+        }
+        if let Some((copy_source, copy_dest)) = opts.copy_output {
+            copy(&copy_source, &copy_dest).unwrap_or_else(|_| {
+                panic!(
+                    "process wrapper error: failed to copy {} into {}",
+                    copy_source, copy_dest
+                )
+            });
+        }
+    }
+
+    exit(status.code().unwrap())
+}
diff --git a/util/process_wrapper/options.rs b/util/process_wrapper/options.rs
new file mode 100644
index 0000000..24bba9f
--- /dev/null
+++ b/util/process_wrapper/options.rs
@@ -0,0 +1,226 @@
+use std::collections::HashMap;
+use std::env;
+use std::fmt;
+use std::process::exit;
+
+use crate::flags::{FlagParseError, Flags, ParseOutcome};
+use crate::util::*;
+
+#[derive(Debug)]
+pub(crate) enum OptionError {
+    FlagError(FlagParseError),
+    Generic(String),
+}
+
+impl fmt::Display for OptionError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::FlagError(e) => write!(f, "error parsing flags: {}", e),
+            Self::Generic(s) => write!(f, "{}", s),
+        }
+    }
+}
+
+#[derive(Debug)]
+pub(crate) struct Options {
+    // Contains the path to the child executable
+    pub(crate) executable: String,
+    // Contains arguments for the child process fetched from files.
+    pub(crate) child_arguments: Vec<String>,
+    // Contains environment variables for the child process fetched from files.
+    pub(crate) child_environment: HashMap<String, String>,
+    // If set, create the specified file after the child process successfully
+    // terminated its execution.
+    pub(crate) touch_file: Option<String>,
+    // If set to (source, dest) copies the source file to dest.
+    pub(crate) copy_output: Option<(String, String)>,
+    // If set, redirects the child process stdout to this file.
+    pub(crate) stdout_file: Option<String>,
+    // If set, redirects the child process stderr to this file.
+    pub(crate) stderr_file: Option<String>,
+}
+
+pub(crate) fn options() -> Result<Options, OptionError> {
+    // Process argument list until -- is encountered.
+    // Everything after is sent to the child process.
+    let mut subst_mapping_raw = None;
+    let mut volatile_status_file_raw = None;
+    let mut env_file_raw = None;
+    let mut arg_file_raw = None;
+    let mut touch_file = None;
+    let mut copy_output_raw = None;
+    let mut stdout_file = None;
+    let mut stderr_file = None;
+    let mut flags = Flags::new();
+    flags.define_repeated_flag("--subst", "", &mut subst_mapping_raw);
+    flags.define_flag("--volatile-status-file", "", &mut volatile_status_file_raw);
+    flags.define_repeated_flag(
+        "--env-file",
+        "File(s) containing environment variables to pass to the child process.",
+        &mut env_file_raw,
+    );
+    flags.define_repeated_flag(
+        "--arg-file",
+        "File(s) containing command line arguments to pass to the child process.",
+        &mut arg_file_raw,
+    );
+    flags.define_flag(
+        "--touch-file",
+        "Create this file after the child process runs successfully.",
+        &mut touch_file,
+    );
+    flags.define_repeated_flag("--copy-output", "", &mut copy_output_raw);
+    flags.define_flag(
+        "--stdout-file",
+        "Redirect subprocess stdout in this file.",
+        &mut stdout_file,
+    );
+    flags.define_flag(
+        "--stderr-file",
+        "Redirect subprocess stderr in this file.",
+        &mut stderr_file,
+    );
+
+    let mut child_args = match flags
+        .parse(env::args().collect())
+        .map_err(OptionError::FlagError)?
+    {
+        ParseOutcome::Help(help) => {
+            eprintln!("{}", help);
+            exit(0);
+        }
+        ParseOutcome::Parsed(p) => p,
+    };
+    let current_dir = std::env::current_dir()
+        .map_err(|e| OptionError::Generic(format!("failed to get current directory: {}", e)))?
+        .to_str()
+        .ok_or_else(|| OptionError::Generic("current directory not utf-8".to_owned()))?
+        .to_owned();
+    let subst_mappings = subst_mapping_raw
+        .unwrap_or_default()
+        .into_iter()
+        .map(|arg| {
+            let (key, val) = arg.split_once('=').ok_or_else(|| {
+                OptionError::Generic(format!("empty key for substitution '{}'", arg))
+            })?;
+            let v = if val == "${pwd}" {
+                current_dir.as_str()
+            } else {
+                val
+            }
+            .to_owned();
+            Ok((key.to_owned(), v))
+        })
+        .collect::<Result<Vec<(String, String)>, OptionError>>()?;
+    let stamp_mappings =
+        volatile_status_file_raw.map_or_else(Vec::new, |s| read_stamp_status_to_array(s).unwrap());
+
+    let environment_file_block = env_from_files(env_file_raw.unwrap_or_default())?;
+    let mut file_arguments = args_from_file(arg_file_raw.unwrap_or_default())?;
+    // Process --copy-output
+    let copy_output = copy_output_raw
+        .map(|co| {
+            if co.len() != 2 {
+                return Err(OptionError::Generic(format!(
+                    "\"--copy-output\" needs exactly 2 parameters, {} provided",
+                    co.len()
+                )));
+            }
+            let copy_source = &co[0];
+            let copy_dest = &co[1];
+            if copy_source == copy_dest {
+                return Err(OptionError::Generic(format!(
+                    "\"--copy-output\" source ({}) and dest ({}) need to be different.",
+                    copy_source, copy_dest
+                )));
+            }
+            Ok((copy_source.to_owned(), copy_dest.to_owned()))
+        })
+        .transpose()?;
+
+    // Prepare the environment variables, unifying those read from files with the ones
+    // of the current process.
+    let vars = environment_block(environment_file_block, &stamp_mappings, &subst_mappings);
+    // Append all the arguments fetched from files to those provided via command line.
+    child_args.append(&mut file_arguments);
+    let child_args = prepare_args(child_args, &subst_mappings);
+    // Split the executable path from the rest of the arguments.
+    let (exec_path, args) = child_args.split_first().ok_or_else(|| {
+        OptionError::Generic(
+            "at least one argument after -- is required (the child process path)".to_owned(),
+        )
+    })?;
+
+    Ok(Options {
+        executable: exec_path.to_owned(),
+        child_arguments: args.to_vec(),
+        child_environment: vars,
+        touch_file,
+        copy_output,
+        stdout_file,
+        stderr_file,
+    })
+}
+
+fn args_from_file(paths: Vec<String>) -> Result<Vec<String>, OptionError> {
+    let mut args = vec![];
+    for path in paths.into_iter() {
+        let mut lines = read_file_to_array(path).map_err(OptionError::Generic)?;
+        args.append(&mut lines);
+    }
+    Ok(args)
+}
+
+fn env_from_files(paths: Vec<String>) -> Result<HashMap<String, String>, OptionError> {
+    let mut env_vars = HashMap::new();
+    for path in paths.into_iter() {
+        let lines = read_file_to_array(path).map_err(OptionError::Generic)?;
+        for line in lines.into_iter() {
+            let (k, v) = line
+                .split_once('=')
+                .ok_or_else(|| OptionError::Generic("environment file invalid".to_owned()))?;
+            env_vars.insert(k.to_owned(), v.to_owned());
+        }
+    }
+    Ok(env_vars)
+}
+
+fn prepare_args(mut args: Vec<String>, subst_mappings: &[(String, String)]) -> Vec<String> {
+    for (f, replace_with) in subst_mappings {
+        for arg in args.iter_mut() {
+            let from = format!("${{{}}}", f);
+            let new = arg.replace(from.as_str(), replace_with);
+            *arg = new;
+        }
+    }
+    args
+}
+
+fn environment_block(
+    environment_file_block: HashMap<String, String>,
+    stamp_mappings: &[(String, String)],
+    subst_mappings: &[(String, String)],
+) -> HashMap<String, String> {
+    // Taking all environment variables from the current process
+    // and sending them down to the child process
+    let mut environment_variables: HashMap<String, String> = std::env::vars().collect();
+    // Have the last values added take precedence over the first.
+    // This is simpler than needing to track duplicates and explicitly override
+    // them.
+    environment_variables.extend(environment_file_block.into_iter());
+    for (f, replace_with) in stamp_mappings {
+        for value in environment_variables.values_mut() {
+            let from = format!("{{{}}}", f);
+            let new = value.replace(from.as_str(), replace_with);
+            *value = new;
+        }
+    }
+    for (f, replace_with) in subst_mappings {
+        for value in environment_variables.values_mut() {
+            let from = format!("${{{}}}", f);
+            let new = value.replace(from.as_str(), replace_with);
+            *value = new;
+        }
+    }
+    environment_variables
+}
diff --git a/util/process_wrapper/process_wrapper.cc b/util/process_wrapper/process_wrapper.cc
deleted file mode 100644
index 4f07c01..0000000
--- a/util/process_wrapper/process_wrapper.cc
+++ /dev/null
@@ -1,225 +0,0 @@
-// Copyright 2020 The Bazel Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//    http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include <algorithm>
-#include <fstream>
-#include <iostream>
-#include <string>
-#include <utility>
-
-#include "util/process_wrapper/system.h"
-#include "util/process_wrapper/utils.h"
-
-using CharType = process_wrapper::System::StrType::value_type;
-
-// Simple process wrapper allowing us to not depend on the shell to run a
-// process to perform basic operations like capturing the output or having
-// the $pwd used in command line arguments or environment variables
-int PW_MAIN(int argc, const CharType* argv[], const CharType* envp[]) {
-  using namespace process_wrapper;
-
-  System::EnvironmentBlock environment_block;
-  // Taking all environment variables from the current process
-  // and sending them down to the child process
-  for (int i = 0; envp[i] != nullptr; ++i) {
-    environment_block.push_back(envp[i]);
-  }
-
-  System::EnvironmentBlock environment_file_block;
-
-  using Subst = std::pair<System::StrType, System::StrType>;
-
-  System::StrType exec_path;
-  std::vector<Subst> subst_mappings;
-  std::vector<Subst> stamp_mappings;
-  System::StrType volatile_status_file;
-  System::StrType stdout_file;
-  System::StrType stderr_file;
-  System::StrType touch_file;
-  System::StrType copy_source;
-  System::StrType copy_dest;
-  System::Arguments arguments;
-  System::Arguments file_arguments;
-
-  // Processing current process argument list until -- is encountered
-  // everthing after gets sent down to the child process
-  for (int i = 1; i < argc; ++i) {
-    System::StrType arg = argv[i];
-    if (++i == argc) {
-      std::cerr << "process wrapper error: argument \"" << ToUtf8(arg)
-                << "\" missing parameter.\n";
-      return -1;
-    }
-    if (arg == PW_SYS_STR("--subst")) {
-      System::StrType subst = argv[i];
-      size_t equal_pos = subst.find('=');
-      if (equal_pos == std::string::npos) {
-        std::cerr << "process wrapper error: wrong substituion format for \""
-                  << ToUtf8(subst) << "\".\n";
-        return -1;
-      }
-      System::StrType key = subst.substr(0, equal_pos);
-      if (key.empty()) {
-        std::cerr << "process wrapper error: empty key for substituion \""
-                  << ToUtf8(subst) << "\".\n";
-        return -1;
-      }
-      System::StrType value = subst.substr(equal_pos + 1, subst.size());
-      if (value == PW_SYS_STR("${pwd}")) {
-        value = System::GetWorkingDirectory();
-      }
-      subst_mappings.push_back({std::move(key), std::move(value)});
-    } else if (arg == PW_SYS_STR("--volatile-status-file")) {
-      if (!volatile_status_file.empty()) {
-        std::cerr << "process wrapper error: \"--volatile-status-file\" can "
-                     "only appear once.\n";
-        return -1;
-      }
-      if (!ReadStampStatusToArray(argv[i], stamp_mappings)) {
-        return -1;
-      }
-    } else if (arg == PW_SYS_STR("--env-file")) {
-      if (!ReadFileToArray(argv[i], environment_file_block)) {
-        return -1;
-      }
-    } else if (arg == PW_SYS_STR("--arg-file")) {
-      if (!ReadFileToArray(argv[i], file_arguments)) {
-        return -1;
-      }
-    } else if (arg == PW_SYS_STR("--touch-file")) {
-      if (!touch_file.empty()) {
-        std::cerr << "process wrapper error: \"--touch-file\" can only appear "
-                     "once.\n";
-        return -1;
-      }
-      touch_file = argv[i];
-    } else if (arg == PW_SYS_STR("--copy-output")) {
-      // i is already at the first arg position, accountint we need another arg
-      // and then -- executable_name.
-      if (i + 1 > argc) {
-        std::cerr
-            << "process wrapper error: \"--copy-output\" needs 2 parameters.\n";
-        return -1;
-      }
-      if (!copy_source.empty() || !copy_dest.empty()) {
-        std::cerr << "process wrapper error: \"--copy-output\" can only appear "
-                     "once.\n";
-        return -1;
-      }
-      copy_source = argv[i];
-      copy_dest = argv[++i];
-      if (copy_source == copy_dest) {
-        std::cerr << "process wrapper error: \"--copy-output\" source and dest "
-                     "need to be different.\n";
-        return -1;
-      }
-    } else if (arg == PW_SYS_STR("--stdout-file")) {
-      if (!stdout_file.empty()) {
-        std::cerr << "process wrapper error: \"--stdout-file\" can only appear "
-                     "once.\n";
-        return -1;
-      }
-      stdout_file = argv[i];
-    } else if (arg == PW_SYS_STR("--stderr-file")) {
-      if (!stderr_file.empty()) {
-        std::cerr << "process wrapper error: \"--stderr-file\" can only appear "
-                     "once.\n";
-        return -1;
-      }
-      stderr_file = argv[i];
-    } else if (arg == PW_SYS_STR("--")) {
-      exec_path = argv[i];
-      for (++i; i < argc; ++i) {
-        arguments.push_back(argv[i]);
-      }
-      // after we consume all arguments we append the files arguments
-      for (const System::StrType& file_arg : file_arguments) {
-        arguments.push_back(file_arg);
-      }
-    } else {
-      std::cerr << "process wrapper error: unknow argument \"" << ToUtf8(arg)
-                << "\"." << '\n';
-      return -1;
-    }
-  }
-
-  // Stamp any format string in an environment variable block
-  for (const Subst& stamp : stamp_mappings) {
-    System::StrType token = PW_SYS_STR("{");
-    token += stamp.first;
-    token.push_back('}');
-    for (System::StrType& env : environment_file_block) {
-      ReplaceToken(env, token, stamp.second);
-    }
-  }
-
-  // Join environment variables arrays
-  environment_block.reserve(environment_block.size() +
-                            environment_file_block.size());
-  environment_block.insert(environment_block.end(),
-                           environment_file_block.begin(),
-                           environment_file_block.end());
-
-  if (subst_mappings.size()) {
-    for (const Subst& subst : subst_mappings) {
-      System::StrType token = PW_SYS_STR("${");
-      token += subst.first;
-      token.push_back('}');
-      for (System::StrType& arg : arguments) {
-        ReplaceToken(arg, token, subst.second);
-      }
-
-      for (System::StrType& env : environment_block) {
-        ReplaceToken(env, token, subst.second);
-      }
-    }
-  }
-
-  // Have the last values added take precedence over the first.
-  // This is simpler than needing to track duplicates and explicitly override
-  // them.
-  std::reverse(environment_block.begin(), environment_block.end());
-
-  int exit_code = System::Exec(exec_path, arguments, environment_block,
-                               stdout_file, stderr_file);
-  if (exit_code == 0) {
-    if (!touch_file.empty()) {
-      std::ofstream file(touch_file);
-      if (file.fail()) {
-        std::cerr << "process wrapper error: failed to create touch file: \""
-                  << ToUtf8(touch_file) << "\"\n";
-        return -1;
-      }
-      file.close();
-    }
-
-    // we perform a copy of the output if necessary
-    if (!copy_source.empty() && !copy_dest.empty()) {
-      std::ifstream source(copy_source, std::ios::binary);
-      if (source.fail()) {
-        std::cerr << "process wrapper error: failed to open copy source: \""
-                  << ToUtf8(copy_source) << "\"\n";
-        return -1;
-      }
-      std::ofstream dest(copy_dest, std::ios::binary);
-      if (dest.fail()) {
-        std::cerr << "process wrapper error: failed to open copy dest: \""
-                  << ToUtf8(copy_dest) << "\"\n";
-        return -1;
-      }
-      dest << source.rdbuf();
-    }
-  }
-  return exit_code;
-}
diff --git a/util/process_wrapper/process_wrapper_fake.cc b/util/process_wrapper/process_wrapper_fake.cc
new file mode 100644
index 0000000..a0b6869
--- /dev/null
+++ b/util/process_wrapper/process_wrapper_fake.cc
@@ -0,0 +1,4 @@
+int main() {
+    // Noop on purpose.
+    return 0;
+}
\ No newline at end of file
diff --git a/util/process_wrapper/system.h b/util/process_wrapper/system.h
deleted file mode 100644
index 8abf744..0000000
--- a/util/process_wrapper/system.h
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright 2020 The Bazel Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//    http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef LIB_PROCESS_WRAPPER_SYSTEM_H_
-#define LIB_PROCESS_WRAPPER_SYSTEM_H_
-
-#include <string>
-#include <vector>
-
-#if defined(_WIN32) && defined(UNICODE)
-#define PW_WIN_UNICODE
-#endif  // defined(_WIN32) && defined(UNICODE)
-
-#if defined(PW_WIN_UNICODE)
-#define PW_SYS_STR(str) L##str
-#define PW_MAIN wmain
-#else
-#define PW_SYS_STR(str) str
-#define PW_MAIN main
-#endif
-
-namespace process_wrapper {
-
-class System {
- public:
-#if defined(PW_WIN_UNICODE)
-  using StrType = std::wstring;
-#else
-  using StrType = std::string;
-#endif  // defined(PW_WIN_UNICODE)
-
-  using StrVecType = std::vector<StrType>;
-  using Arguments = StrVecType;
-  using EnvironmentBlock = StrVecType;
-
- public:
-  // Gets the working directory of the current process
-  static StrType GetWorkingDirectory();
-
-  // Simple function to execute a process that inherits all the current
-  // process handles.
-  // Even if the function doesn't modify global state it is not reentrant
-  // It is meant to be called once during the lifetime of the parent process
-  static int Exec(const StrType& executable, const Arguments& arguments,
-                  const EnvironmentBlock& environment_block,
-                  const StrType& stdout_file, const StrType& stderr_file);
-};
-
-}  // namespace process_wrapper
-
-#endif  // LIB_PROCESS_WRAPPER_SYSTEM_H_
diff --git a/util/process_wrapper/system_posix.cc b/util/process_wrapper/system_posix.cc
deleted file mode 100644
index 45ff9e3..0000000
--- a/util/process_wrapper/system_posix.cc
+++ /dev/null
@@ -1,197 +0,0 @@
-// Copyright 2020 The Bazel Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//    http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "util/process_wrapper/system.h"
-#include "util/process_wrapper/utils.h"
-
-// posix headers
-#include <fcntl.h>
-#include <signal.h>
-#include <sys/stat.h>
-#include <sys/types.h>
-#include <sys/wait.h>
-#include <unistd.h>
-
-#include <cerrno>
-#include <cstring>
-#include <iostream>
-#include <vector>
-
-namespace process_wrapper {
-
-namespace {
-
-class OutputPipe {
- public:
-  static constexpr size_t kReadEndDesc = 0;
-  static constexpr size_t kWriteEndDesc = 1;
-
-  ~OutputPipe() {
-    CloseReadEnd();
-    CloseWriteEnd();
-  }
-
-  int CreateEnds() {
-    if (pipe(output_pipe_desc_) != 0) {
-      std::cerr << "process wrapper error: failed to open the stdout pipes.\n";
-      return false;
-    }
-    return true;
-  }
-  void DupWriteEnd(int newfd) {
-    dup2(output_pipe_desc_[kWriteEndDesc], newfd);
-    CloseReadEnd();
-    CloseWriteEnd();
-  }
-
-  void CloseReadEnd() { Close(kReadEndDesc); }
-  void CloseWriteEnd() { Close(kWriteEndDesc); }
-
-  int ReadEndDesc() const { return output_pipe_desc_[kReadEndDesc]; }
-  int WriteEndDesc() const { return output_pipe_desc_[kWriteEndDesc]; }
-
-  bool WriteToFile(const System::StrType &stdout_file) {
-    CloseWriteEnd();
-
-    constexpr size_t kBufferSize = 4096;
-    char buffer[kBufferSize];
-    int output_file_desc =
-        open(stdout_file.c_str(), O_WRONLY | O_CREAT | O_TRUNC,
-             S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
-    if (output_file_desc == -1) {
-      std::cerr << "process wrapper error: failed to open redirection file: "
-                << std::strerror(errno) << ".\n";
-      return false;
-    }
-    while (1) {
-      ssize_t read_bytes = read(ReadEndDesc(), buffer, kBufferSize);
-      if (read_bytes < 0) {
-        std::cerr
-            << "process wrapper error: failed to read child process output: "
-            << std::strerror(errno) << ".\n";
-        return false;
-      } else if (read_bytes == 0) {
-        break;
-      }
-      ssize_t written_bytes = write(output_file_desc, buffer, read_bytes);
-      if (written_bytes < 0 || written_bytes != read_bytes) {
-        std::cerr << "process wrapper error: failed to write to ouput file: "
-                  << std::strerror(errno) << ".\n";
-        return false;
-      }
-    }
-
-    CloseReadEnd();
-    close(output_file_desc);
-    return true;
-  }
-
- private:
-  void Close(size_t idx) {
-    if (output_pipe_desc_[idx] > 0) {
-      close(output_pipe_desc_[idx]);
-    }
-    output_pipe_desc_[idx] = -1;
-  }
-  int output_pipe_desc_[2] = {-1};
-};
-
-}  // namespace
-
-System::StrType System::GetWorkingDirectory() {
-  const size_t kMaxBufferLength = 4096;
-  char cwd[kMaxBufferLength];
-  if (getcwd(cwd, sizeof(cwd)) == NULL) {
-    return System::StrType{};
-  }
-  return System::StrType{cwd};
-}
-
-int System::Exec(const System::StrType &executable,
-                 const System::Arguments &arguments,
-                 const System::EnvironmentBlock &environment_block,
-                 const StrType &stdout_file, const StrType &stderr_file) {
-  OutputPipe stdout_pipe;
-  if (!stdout_file.empty() && !stdout_pipe.CreateEnds()) {
-    return -1;
-  }
-  OutputPipe stderr_pipe;
-  if (!stderr_file.empty() && !stderr_pipe.CreateEnds()) {
-    return -1;
-  }
-
-  pid_t child_pid = fork();
-  if (child_pid < 0) {
-    std::cerr << "process wrapper error: failed to fork the current process: "
-              << std::strerror(errno) << ".\n";
-    return -1;
-  } else if (child_pid == 0) {
-    if (!stdout_file.empty()) {
-      stdout_pipe.DupWriteEnd(STDOUT_FILENO);
-    }
-    if (!stderr_file.empty()) {
-      stderr_pipe.DupWriteEnd(STDERR_FILENO);
-    }
-    std::vector<char *> argv;
-    argv.push_back(const_cast<char *>(executable.c_str()));
-    for (const StrType &argument : arguments) {
-      argv.push_back(const_cast<char *>(argument.c_str()));
-    }
-    argv.push_back(nullptr);
-
-    std::vector<char *> envp;
-    for (const StrType &ev : environment_block) {
-      envp.push_back(const_cast<char *>(ev.c_str()));
-    }
-    envp.push_back(nullptr);
-
-    umask(022);
-
-    execve(executable.c_str(), argv.data(), envp.data());
-    std::cerr << "process wrapper error: failed to exec the new process: "
-              << std::strerror(errno) << ".\n";
-    return -1;
-  }
-
-  if (!stdout_file.empty()) {
-    if (!stdout_pipe.WriteToFile(stdout_file)) {
-      return -1;
-    }
-  }
-  if (!stderr_file.empty()) {
-    if (!stderr_pipe.WriteToFile(stderr_file)) {
-      return -1;
-    }
-  }
-
-  int err, exit_status;
-  do {
-    err = waitpid(child_pid, &exit_status, 0);
-  } while (err == -1 && errno == EINTR);
-
-  if (WIFEXITED(exit_status)) {
-    return WEXITSTATUS(exit_status);
-  } else if (WIFSIGNALED(exit_status)) {
-    raise(WTERMSIG(exit_status));
-  } else if (WIFSTOPPED(exit_status)) {
-    raise(WSTOPSIG(exit_status));
-  } else {
-    std::cerr << "process wrapper error: failed to parse exit code of the "
-                 "child process: "
-              << exit_status << ".\n";
-  }
-  return -1;
-}
-
-}  // namespace process_wrapper
diff --git a/util/process_wrapper/system_windows.cc b/util/process_wrapper/system_windows.cc
deleted file mode 100644
index 6131102..0000000
--- a/util/process_wrapper/system_windows.cc
+++ /dev/null
@@ -1,297 +0,0 @@
-// Copyright 2020 The Bazel Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//    http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include <cstddef>
-
-#include "util/process_wrapper/system.h"
-
-#ifndef WIN32_LEAN_AND_MEAN
-#define WIN32_LEAN_AND_MEAN
-#endif
-
-#include <windows.h>
-
-#include <iostream>
-
-namespace process_wrapper {
-
-namespace {
-
-// We need to follow specific quoting rules for maximum compatibility as
-// explained here:
-// https://docs.microsoft.com/en-us/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way
-void ArgumentQuote(const System::StrType& argument,
-                   System::StrType& command_line) {
-  if (argument.empty() == false &&
-      argument.find_first_of(PW_SYS_STR(" \t\n\v\"")) == argument.npos) {
-    command_line.append(argument);
-  } else {
-    command_line.push_back(PW_SYS_STR('"'));
-
-    for (auto it = argument.begin();; ++it) {
-      unsigned number_backslashes = 0;
-
-      while (it != argument.end() && *it == PW_SYS_STR('\\')) {
-        ++it;
-        ++number_backslashes;
-      }
-
-      if (it == argument.end()) {
-        command_line.append(number_backslashes * 2, PW_SYS_STR('\\'));
-        break;
-      } else if (*it == L'"') {
-        command_line.append(number_backslashes * 2 + 1, PW_SYS_STR('\\'));
-        command_line.push_back(*it);
-      } else {
-        command_line.append(number_backslashes, PW_SYS_STR('\\'));
-        command_line.push_back(*it);
-      }
-    }
-    command_line.push_back(PW_SYS_STR('"'));
-  }
-}
-
-// Arguments needs to be quoted and space separated
-void MakeCommandLine(const System::Arguments& arguments,
-                     System::StrType& command_line) {
-  for (const System::StrType& argument : arguments) {
-    command_line.push_back(PW_SYS_STR(' '));
-    ArgumentQuote(argument, command_line);
-  }
-}
-
-// Environment variables are \0 separated
-void MakeEnvironmentBlock(const System::EnvironmentBlock& environment_block,
-                          System::StrType& environment_block_win) {
-  for (const System::StrType& ev : environment_block) {
-    environment_block_win += ev;
-    environment_block_win.push_back(PW_SYS_STR('\0'));
-  }
-  environment_block_win.push_back(PW_SYS_STR('\0'));
-}
-
-std::string GetLastErrorAsStr() {
-  LPVOID msg_buffer = nullptr;
-  size_t size = ::FormatMessageA(
-      FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
-          FORMAT_MESSAGE_IGNORE_INSERTS,
-      NULL, ::GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
-      (LPSTR)&msg_buffer, 0, NULL);
-  std::string error((LPSTR)msg_buffer, size);
-  LocalFree(msg_buffer);
-  return error;
-}
-
-class OutputPipe {
- public:
-  static constexpr size_t kReadEndHandle = 0;
-  static constexpr size_t kWriteEndHandle = 1;
-
-  ~OutputPipe() {
-    CloseReadEnd();
-    CloseWriteEnd();
-  }
-
-  bool CreateEnds(SECURITY_ATTRIBUTES& saAttr) {
-    if (!::CreatePipe(&output_pipe_handles_[kReadEndHandle],
-                      &output_pipe_handles_[kWriteEndHandle], &saAttr, 0)) {
-      return false;
-    }
-
-    if (!::SetHandleInformation(output_pipe_handles_[kReadEndHandle],
-                                HANDLE_FLAG_INHERIT, 0)) {
-      return false;
-    }
-
-    return true;
-  }
-
-  void CloseReadEnd() { Close(kReadEndHandle); }
-  void CloseWriteEnd() { Close(kWriteEndHandle); }
-
-  HANDLE ReadEndHandle() const { return output_pipe_handles_[kReadEndHandle]; }
-  HANDLE WriteEndHandle() const {
-    return output_pipe_handles_[kWriteEndHandle];
-  }
-
-  bool WriteToFile(const System::StrType& stdout_file) {
-    CloseWriteEnd();
-    HANDLE output_file_handle = CreateFile(
-        /*lpFileName*/ stdout_file.c_str(),
-        /*dwDesiredAccess*/ GENERIC_WRITE,
-        /*dwShareMode*/ FILE_SHARE_WRITE,
-        /*lpSecurityAttributes*/ NULL,
-        /*dwCreationDisposition*/ CREATE_ALWAYS,
-        /*dwFlagsAndAttributes*/ FILE_ATTRIBUTE_NORMAL,
-        /*hTemplateFile*/ NULL);
-
-    if (output_file_handle == INVALID_HANDLE_VALUE) {
-      std::cerr << "process wrapper error: failed to open the output file: "
-                << GetLastErrorAsStr();
-      return false;
-    }
-
-    constexpr DWORD kBufferSize = 4096;
-    CHAR buffer[kBufferSize];
-    bool ret = true;
-    while (1) {
-      DWORD read;
-      bool success =
-          ReadFile(ReadEndHandle(), buffer, kBufferSize, &read, NULL);
-      if (read == 0) {
-        break;
-      } else if (!success) {
-        std::cerr
-            << "process wrapper error: failed to read child process output: "
-            << GetLastErrorAsStr();
-        ret = false;
-        break;
-      }
-
-      DWORD written;
-      success = WriteFile(output_file_handle, buffer, read, &written, NULL);
-      if (!success) {
-        std::cerr << "process wrapper error: failed to write to output capture "
-                     "file: "
-                  << GetLastErrorAsStr();
-        ret = false;
-        break;
-      }
-    }
-    CloseHandle(output_file_handle);
-    return ret;
-  }
-
- private:
-  void Close(size_t idx) {
-    if (output_pipe_handles_[idx] != nullptr) {
-      ::CloseHandle(output_pipe_handles_[idx]);
-    }
-    output_pipe_handles_[idx] = nullptr;
-  }
-  HANDLE output_pipe_handles_[2] = {nullptr};
-};
-
-}  // namespace
-
-System::StrType System::GetWorkingDirectory() {
-  constexpr DWORD kMaxBufferLength = 4096;
-  TCHAR buffer[kMaxBufferLength];
-  if (::GetCurrentDirectory(kMaxBufferLength, buffer) == 0) {
-    return System::StrType{};
-  }
-  return System::StrType{buffer};
-}
-
-int System::Exec(const System::StrType& executable,
-                 const System::Arguments& arguments,
-                 const System::EnvironmentBlock& environment_block,
-                 const StrType& stdout_file, const StrType& stderr_file) {
-  STARTUPINFO startup_info;
-  ZeroMemory(&startup_info, sizeof(STARTUPINFO));
-  startup_info.cb = sizeof(STARTUPINFO);
-
-  OutputPipe stdout_pipe;
-  OutputPipe stderr_pipe;
-
-  if (!stdout_file.empty() || !stderr_file.empty()) {
-    // We will be setting our own stdout/stderr handles. Note that when setting `STARTF_USESTDHANDLES`
-    // it is critical to set *all* handles or the child process might get a null handle (or garbage).
-    startup_info.dwFlags |= STARTF_USESTDHANDLES;
-    startup_info.hStdInput = INVALID_HANDLE_VALUE;
-
-    SECURITY_ATTRIBUTES saAttr;
-    ZeroMemory(&saAttr, sizeof(SECURITY_ATTRIBUTES));
-    saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
-    saAttr.bInheritHandle = TRUE;
-    saAttr.lpSecurityDescriptor = NULL;
-
-    if (!stdout_file.empty()) {
-      if (!stdout_pipe.CreateEnds(saAttr)) {
-        std::cerr << "process wrapper error: failed to create stdout pipe: "
-                  << GetLastErrorAsStr();
-        return -1;
-      }
-      startup_info.hStdOutput = stdout_pipe.WriteEndHandle();
-    } else {
-      startup_info.hStdOutput = INVALID_HANDLE_VALUE;
-    }
-
-    if (!stderr_file.empty()) {
-      if (!stderr_pipe.CreateEnds(saAttr)) {
-        std::cerr << "process wrapper error: failed to create stderr pipe: "
-                  << GetLastErrorAsStr();
-        return -1;
-      }
-      startup_info.hStdError = stderr_pipe.WriteEndHandle();
-    } else {
-      startup_info.hStdError = INVALID_HANDLE_VALUE;
-    }
-  }
-
-  System::StrType command_line;
-  ArgumentQuote(executable, command_line);
-  MakeCommandLine(arguments, command_line);
-
-  System::StrType environment_block_win;
-  MakeEnvironmentBlock(environment_block, environment_block_win);
-
-  PROCESS_INFORMATION process_info;
-  ZeroMemory(&process_info, sizeof(PROCESS_INFORMATION));
-
-  BOOL success = ::CreateProcess(
-      /*lpApplicationName*/ nullptr,
-      /*lpCommandLine*/ command_line.empty() ? nullptr : &command_line[0],
-      /*lpProcessAttributes*/ nullptr,
-      /*lpThreadAttributes*/ nullptr,
-      /*bInheritHandles*/ TRUE,
-      /*dwCreationFlags*/ 0
-#if defined(UNICODE)
-          | CREATE_UNICODE_ENVIRONMENT
-#endif  // defined(UNICODE)
-      ,
-      /*lpEnvironment*/ environment_block_win.empty()
-          ? nullptr
-          : &environment_block_win[0],
-      /*lpCurrentDirectory*/ nullptr,
-      /*lpStartupInfo*/ &startup_info,
-      /*lpProcessInformation*/ &process_info);
-
-  if (success == FALSE) {
-    std::cerr << "process wrapper error: failed to launch a new process: "
-              << GetLastErrorAsStr();
-    return -1;
-  }
-
-  if (!stdout_file.empty()) {
-    if (!stdout_pipe.WriteToFile(stdout_file)) {
-      return -1;
-    }
-  }
-  if (!stderr_file.empty()) {
-    if (!stderr_pipe.WriteToFile(stderr_file)) {
-      return -1;
-    }
-  }
-
-  DWORD exit_status;
-  WaitForSingleObject(process_info.hProcess, INFINITE);
-  if (GetExitCodeProcess(process_info.hProcess, &exit_status) == FALSE)
-    exit_status = -1;
-  CloseHandle(process_info.hThread);
-  CloseHandle(process_info.hProcess);
-  return exit_status;
-}
-
-}  // namespace process_wrapper
diff --git a/util/process_wrapper/util.rs b/util/process_wrapper/util.rs
new file mode 100644
index 0000000..4b3d6bb
--- /dev/null
+++ b/util/process_wrapper/util.rs
@@ -0,0 +1,103 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use std::fs::File;
+use std::io::{BufRead, BufReader, Read};
+
+pub(crate) fn read_file_to_array(path: String) -> Result<Vec<String>, String> {
+    let file = File::open(path).map_err(|e| e.to_string())?;
+    read_to_array(file)
+}
+
+pub(crate) fn read_stamp_status_to_array(path: String) -> Result<Vec<(String, String)>, String> {
+    let file = File::open(path).map_err(|e| e.to_string())?;
+    stamp_status_to_array(file)
+}
+
+fn read_to_array(reader: impl Read) -> Result<Vec<String>, String> {
+    let reader = BufReader::new(reader);
+    let mut ret = vec![];
+    let mut escaped_line = String::new();
+    for l in reader.lines() {
+        let line = l.map_err(|e| e.to_string())?;
+        if line.is_empty() {
+            continue;
+        }
+        // a \ at the end of a line allows us to escape the new line break,
+        // \\ yields a single \, so \\\ translates to a single \ and a new line
+        // escape
+        let end_backslash_count = line.chars().rev().take_while(|&c| c == '\\').count();
+        // a 0 or even number of backslashes do not lead to a new line escape
+        let escape = end_backslash_count % 2 == 1;
+        //  remove backslashes and add back two for every one
+        let l = line.trim_end_matches('\\');
+        escaped_line.push_str(l);
+        for _ in 0..end_backslash_count / 2 {
+            escaped_line.push('\\');
+        }
+        if escape {
+            // we add a newline as we expect a line after this
+            escaped_line.push('\n');
+        } else {
+            ret.push(escaped_line);
+            escaped_line = String::new();
+        }
+    }
+    Ok(ret)
+}
+
+fn stamp_status_to_array(reader: impl Read) -> Result<Vec<(String, String)>, String> {
+    let escaped_lines = read_to_array(reader)?;
+    escaped_lines
+        .into_iter()
+        .map(|l| {
+            let (s1, s2) = l
+                .split_once(' ')
+                .ok_or_else(|| format!("wrong workspace status file format for \"{}\"", l))?;
+            Ok((s1.to_owned(), s2.to_owned()))
+        })
+        .collect()
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn test_read_to_array() {
+        let input = r#"some escaped \\\
+string
+with other lines"#
+            .to_owned();
+        let expected = vec![
+            r#"some escaped \
+string"#,
+            "with other lines",
+        ];
+        let got = read_to_array(input.as_bytes()).unwrap();
+        assert_eq!(expected, got);
+    }
+
+    #[test]
+    fn test_stamp_status_to_array() {
+        let lines = "aaa bbb\\\nvvv\nccc ddd\neee fff";
+        let got = stamp_status_to_array(lines.as_bytes()).unwrap();
+        let expected = vec![
+            ("aaa".to_owned(), "bbb\nvvv".to_owned()),
+            ("ccc".to_owned(), "ddd".to_owned()),
+            ("eee".to_owned(), "fff".to_owned()),
+        ];
+        assert_eq!(expected, got);
+    }
+}
diff --git a/util/process_wrapper/utils.cc b/util/process_wrapper/utils.cc
deleted file mode 100644
index 00ade0a..0000000
--- a/util/process_wrapper/utils.cc
+++ /dev/null
@@ -1,130 +0,0 @@
-// Copyright 2020 The Bazel Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//    http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "util/process_wrapper/utils.h"
-
-#include <fstream>
-#include <iostream>
-#include <streambuf>
-
-#if defined(PW_WIN_UNICODE)
-#include <codecvt>
-#include <locale>
-#endif  // defined(PW_WIN_UNICODE)
-
-namespace process_wrapper {
-
-System::StrType FromUtf8(const std::string& string) {
-#if defined(PW_WIN_UNICODE)
-  return std::wstring_convert<std::codecvt_utf8<wchar_t>>().from_bytes(string);
-#else
-  return string;
-#endif  // defined(PW_WIN_UNICODE)
-}
-
-std::string ToUtf8(const System::StrType& string) {
-#if defined(PW_WIN_UNICODE)
-  return std::wstring_convert<std::codecvt_utf8<wchar_t>>().to_bytes(string);
-#else
-  return string;
-#endif  // defined(PW_WIN_UNICODE)
-}
-
-void ReplaceToken(System::StrType& str, const System::StrType& token,
-                  const System::StrType& replacement) {
-  std::size_t pos = str.find(token);
-  if (pos != std::string::npos) {
-    str.replace(pos, token.size(), replacement);
-  }
-}
-
-bool ReadFileToArray(const System::StrType& file_path,
-                     System::StrVecType& vec) {
-  std::ifstream file(file_path);
-  if (file.fail()) {
-    std::cerr << "process wrapper error: failed to open file: "
-              << ToUtf8(file_path) << '\n';
-    return false;
-  }
-  std::string read_line, escaped_line;
-  while (std::getline(file, read_line)) {
-    // handle CRLF files when as they might be
-    // written on windows and read from linux
-    if (!read_line.empty() && read_line.back() == '\r') {
-      read_line.pop_back();
-    }
-    // Skip empty lines if any
-    if (read_line.empty()) {
-      continue;
-    }
-
-    // a \ at the end of a line allows us to escape the new line break,
-    // \\ yields a single \, so \\\ translates to a single \ and a new line
-    // escape
-    int end_backslash_count = 0;
-    for (std::string::reverse_iterator rit = read_line.rbegin();
-         rit != read_line.rend() && *rit == '\\'; ++rit) {
-      end_backslash_count++;
-    }
-
-    // a 0 or pair number of backslashes do not lead to a new line escape
-    bool escape = false;
-    if (end_backslash_count & 1) {
-      escape = true;
-    }
-
-    // remove backslashes
-    while (end_backslash_count > 0) {
-      end_backslash_count -= 2;
-      read_line.pop_back();
-    }
-
-    if (escape) {
-      read_line.push_back('\n');
-      escaped_line += read_line;
-    } else {
-      vec.push_back(FromUtf8(escaped_line + read_line));
-      escaped_line.clear();
-    }
-  }
-  return true;
-}
-
-bool ReadStampStatusToArray(
-    const System::StrType& stamp_path,
-    std::vector<std::pair<System::StrType, System::StrType>>& vec) {
-  // Read each line of the stamp file and split on the first space
-  System::StrVecType stamp_block;
-  if (!ReadFileToArray(stamp_path, stamp_block)) {
-    return false;
-  }
-
-  for (System::StrVecType::size_type i = 0; i < stamp_block.size(); ++i) {
-    size_t space_pos = stamp_block[i].find(' ');
-    if (space_pos == std::string::npos) {
-      std::cerr << "process wrapper error: wrong workspace status file "
-                   "format for \""
-                << ToUtf8(stamp_block[i]) << "\".\n";
-      return false;
-    }
-    System::StrType key = stamp_block[i].substr(0, space_pos);
-    System::StrType value =
-        stamp_block[i].substr(space_pos + 1, stamp_block[i].size());
-    vec.push_back({std::move(key), std::move(value)});
-  }
-
-  return true;
-}
-
-}  // namespace process_wrapper
diff --git a/util/process_wrapper/utils.h b/util/process_wrapper/utils.h
deleted file mode 100644
index 62c3da6..0000000
--- a/util/process_wrapper/utils.h
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright 2020 The Bazel Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//    http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef LIB_PROCESS_WRAPPER_UTILS_H_
-#define LIB_PROCESS_WRAPPER_UTILS_H_
-
-#include <string>
-
-#include "util/process_wrapper/system.h"
-
-namespace process_wrapper {
-
-// Converts to and frin the system string format
-System::StrType FromUtf8(const std::string& string);
-std::string ToUtf8(const System::StrType& string);
-
-// Replaces a token in str by replacement
-void ReplaceToken(System::StrType& str, const System::StrType& token,
-                  const System::StrType& replacement);
-
-// Reads a file in text mode and feeds each line to item in the vec output
-bool ReadFileToArray(const System::StrType& file_path, System::StrVecType& vec);
-
-// Reads a workspace status stamp file to an array of key value pairs
-bool ReadStampStatusToArray(
-    const System::StrType& stamp_path,
-    std::vector<std::pair<System::StrType, System::StrType>>& vec);
-
-}  // namespace process_wrapper
-
-#endif  // LIB_PROCESS_WRAPPER_UTILS_H_