Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add process wrapper #257

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .clang-format
@@ -0,0 +1,2 @@
---
BasedOnStyle: Google
173 changes: 173 additions & 0 deletions docs/process_wrapper_doc.md
@@ -0,0 +1,173 @@
## process_wrapper

Process wrapper is a helper that allows you, in a platform independent way,
jelmansouri marked this conversation as resolved.
Show resolved Hide resolved
to not depend on run_shell to perform basic operations like capturing
the output or having $pwd used in command line arguments or environment
variables

Note: This wrapper adds a dependency on a C++ toolchain

It is meant to be used in rules implementations like such:

```python
def _impl(ctx):
stdout_output = ctx.actions.declare_file(ctx.label.name + ".stdout")
args = ctx.actions.args()
args.add("--stdout-file", stdout_output.path)
args.add("--subst", "pwd=${pwd}")
args.add("--")
args.add(ctx.executable._compiler.path)
args.add(ctx.attr.test_config)
args.add("--test-dir", "${pwd}")
env = {"TEST_DIR": "${pwd}/test_path"}

ctx.actions.run(
executable = ctx.executable._process_wrapper,
inputs = ctx.files.env_files + ctx.files.arg_files,
outputs = [stdout_output],
arguments = [args],
env = env,
tools = [ctx.executable._compiler],
)

return [DefaultInfo(files = depset([stdout_output]))]

process_wrapper_tester = rule(
implementation = _impl,
attrs = {
"_compiler": attr.label(
default = "//compiler/label",
executable = True,
cfg = "exec",
),
"_process_wrapper": attr.label(
default = "@bazel_skylib//lib/process_wrapper",
executable = True,
allow_single_file = True,
cfg = "exec",
),
},
)
```

### Parameters

<table class="params-table">
<colgroup>
<col class="col-param" />
<col class="col-description" />
</colgroup>
<tbody>
<tr id="process_wrapper---">
<td><code>-- executable_path [args]</code></td>
<td>
required.
<p>
Everything after -- is used as command line arguments to the child process.
</p>
<p>
executable_path: path to the executable that is going to be launched as a child process.
</p>
<p>
[args]: Arguments to be passed on to the child process.
</p>
</td>
</tr>
<tr id="process_wrapper-subst-pwd">
jelmansouri marked this conversation as resolved.
Show resolved Hide resolved
<td><code>--subst var=val</code></td>
<td>
optional.
<p>
Allows to substitute ${key} by value in all environment variables and arguments passed to the child process.
If value is equal to ${pwd} the actual working directory of the process is substituted.
${key} doesn’t have a corresponding value it is left as is.
If key matches a an existing key there is no escaping mechanism.
</p>
</td>
</tr>
<tr id="process_wrapper-env-file">
<td><code>--env-file file_path</code></td>
<td>
optional.
<p>
Loads an environment variables file form "file_path".
Can appear multiple times.
</p>
<p>
The file consists of new line separated environment variables under the form of VAR=VALUE.
jelmansouri marked this conversation as resolved.
Show resolved Hide resolved
\ at the end of of a line allows you to escape the new line splitting.
</p>
<p>
file_path is subject to system limitations regarding maximum number of characters, which is 260 on windows.
</p>
</td>
</tr>
<tr id="process_wrapper-arg-file">
<td><code>--arg-file file_path</code></td>
<td>
optional.
<p>
Loads a command line arguments file form "file_path".
Can appear multiple times.
</p>
<p>
The file consists of new line separated arguments.
\ at the end of of a line allows you to escape the new line splitting.
</p>
<p>
file_path is subject to system limitations regarding maximum number of characters, which is 260 on windows.
</p>
</td>
</tr>
<tr id="process_wrapper-stdout-file">
<td><code>--stdout-file file_path</code></td>
<td>
optional.
<p>
Writes the standard output of the child process to a file.
Can appear once.
</p>
<p>
file_path is subject to system limitations regarding maximum number of characters, which is 260 on windows.
</p>
</td>
</tr>
<tr id="process_wrapper-stderr-file">
<td><code>--stderr-file file_path</code></td>
<td>
optional.
<p>
Writes the standard error output of the child process to a file.
Can appear once.
</p>
<p>
file_path is subject to system limitations regarding maximum number of characters, which is 260 on windows.
</p>
</td>
</tr>
<tr id="process_wrapper-touch-file">
<td><code>--touch-file file_path</code></td>
jelmansouri marked this conversation as resolved.
Show resolved Hide resolved
<td>
optional.
<p>
Touches the file specified in file_path. The touch file only gets created if the child process exists successfully.
</p>
<p>
file_path is subject to system limitations regarding maximum number of characters, which is 260 on windows.
</p>
</td>
</tr>
<tr id="process_wrapper-copy-ouptut">
<td><code>--copy-output source_path dest_path</code></td>
<td>
optional.
<p>
Copies the source file into destination. Both need to be output files declared in skylark.
</p>
<p>
source_path and dest_path are subject to system limitations regarding maximum number of characters, which is 260 on windows.
</p>
</td>
</tr>
</tbody>
</table>
2 changes: 1 addition & 1 deletion lib/BUILD
Expand Up @@ -87,7 +87,7 @@ filegroup(
# The files needed for distribution
filegroup(
name = "distribution",
srcs = glob(["*"]),
srcs = glob(["**"]),
visibility = [
"//:__pkg__",
"//distribution:__pkg__",
Expand Down
26 changes: 26 additions & 0 deletions lib/process_wrapper/BUILD
@@ -0,0 +1,26 @@
load("@rules_cc//cc:defs.bzl", "cc_binary")

cc_binary(
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": [],
}),
visibility = ["//visibility:public"],
)
175 changes: 175 additions & 0 deletions lib/process_wrapper/process_wrapper.cc
@@ -0,0 +1,175 @@
#include <fstream>
#include <iostream>
#include <string>
#include <utility>

#include "lib/process_wrapper/system.h"
#include "lib/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]);
}

using Subst = std::pair<System::StrType, System::StrType>;

System::StrType exec_path;
std::vector<Subst> subst_mappings;
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("--env-file")) {
if (!ReadFileToArray(argv[i], environment_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;
}
}

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);
}
}
}

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;
}