diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index 8eec83ae11..f3fb75e9f1 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -45,10 +45,6 @@ tasks: - "//test/..." - "@examples//..." - "-//test/conflicting_deps:conflicting_deps_test" - # rust_doc_test is likely not fully sandboxed - - "-//test/chained_direct_deps:mod3_doc_test" - - "-@examples//fibonacci:fibonacci_doc_test" - - "-@examples//hello_lib:hello_lib_doc_test" - "-@examples//ffi/rust_calling_c/simple/..." # See https://github.com/bazelbuild/bazel/issues/9987 - "-@examples//ffi/rust_calling_c:matrix_dylib_test" diff --git a/rust/private/BUILD.bazel b/rust/private/BUILD.bazel index 119af07117..b88dae1188 100644 --- a/rust/private/BUILD.bazel +++ b/rust/private/BUILD.bazel @@ -8,5 +8,6 @@ bzl_library( visibility = ["//rust:__subpackages__"], deps = [ "//rust/platform:rules", + "//util/launcher:bzl_lib", ], ) diff --git a/rust/private/rust.bzl b/rust/private/rust.bzl index 6fc07e5716..15b47920e6 100644 --- a/rust/private/rust.bzl +++ b/rust/private/rust.bzl @@ -20,9 +20,9 @@ load( "crate_name_from_attr", "dedent", "determine_output_hash", - "expand_dict_value_locations", "find_toolchain", ) +load("//util/launcher:launcher.bzl", "create_launcher") # TODO(marco): Separate each rule into its own file. @@ -304,88 +304,6 @@ def _rust_binary_impl(ctx): ), ) -def _create_test_launcher(ctx, toolchain, output, providers): - """Create a process wrapper to ensure runtime environment variables are defined for the test binary - - Args: - ctx (ctx): The rule's context object - toolchain (rust_toolchain): The current rust toolchain - output (File): The output File that will be produced, depends on crate type. - providers (list): Providers from a rust compile action. See `rustc_compile_action` - - Returns: - list: A list of providers similar to `rustc_compile_action` but with modified default info - """ - - # TODO: It's unclear if the toolchain is in the same configuration as the `_launcher` attribute - # This should be investigated but for now, we generally assume if the target environment is windows, - # the execution environment is windows. - if toolchain.os == "windows": - launcher_filename = ctx.label.name + ".launcher.exe" - else: - launcher_filename = ctx.label.name + ".launcher" - - launcher = ctx.actions.declare_file(launcher_filename) - - # Because returned executables must be created from the same rule, the - # launcher target is simply symlinked and exposed. - ctx.actions.symlink( - output = launcher, - target_file = ctx.executable._launcher, - is_executable = True, - ) - - # Get data attribute - data = getattr(ctx.attr, "data", []) - - # Expand the environment variables and write them to a file - environ_file = ctx.actions.declare_file(launcher_filename + ".launchfiles/env") - environ = expand_dict_value_locations( - ctx, - getattr(ctx.attr, "env", {}), - data, - ) - - # Convert the environment variables into a list to be written into a file. - environ_list = [] - for key, value in sorted(environ.items()): - environ_list.extend([key, value]) - - ctx.actions.write( - output = environ_file, - content = "\n".join(environ_list), - ) - - launcher_files = [environ_file] - - # Replace the `DefaultInfo` provider in the returned list - default_info = None - for i in range(len(providers)): - if type(providers[i]) == "DefaultInfo": - default_info = providers[i] - providers.pop(i) - break - - if not default_info: - fail("No DefaultInfo provider returned from `rustc_compile_action`") - - providers.extend([ - DefaultInfo( - files = default_info.files, - runfiles = default_info.default_runfiles.merge( - # The output is now also considered a runfile - ctx.runfiles(files = launcher_files + [output]), - ), - executable = launcher, - ), - OutputGroupInfo( - launcher_files = depset(launcher_files), - output = depset([output]), - ), - ]) - - return providers - def _rust_test_common(ctx, toolchain, output): """Builds a Rust test binary. @@ -452,7 +370,17 @@ def _rust_test_common(ctx, toolchain, output): rust_flags = ["--test"] if ctx.attr.use_libtest_harness else ["--cfg", "test"], ) - return _create_test_launcher(ctx, toolchain, output, providers) + env = getattr(ctx.attr, "env", {}) + data = getattr(ctx.attr, "data", []) + + return create_launcher( + ctx = ctx, + toolchain = toolchain, + providers = providers, + env = env, + data = data, + executable = output, + ) def _rust_test_impl(ctx): """The implementation of the `rust_test` rule diff --git a/rust/private/rustdoc_test.bzl b/rust/private/rustdoc_test.bzl index 7eda389010..c3f8ed5c28 100644 --- a/rust/private/rustdoc_test.bzl +++ b/rust/private/rustdoc_test.bzl @@ -13,8 +13,31 @@ # limitations under the License. # buildifier: disable=module-docstring +load("@bazel_tools//tools/build_defs/cc:action_names.bzl", "C_COMPILE_ACTION_NAME") load("//rust/private:common.bzl", "rust_common") -load("//rust/private:utils.bzl", "find_toolchain", "get_lib_name", "get_preferred_artifact") +load("//rust/private:toolchain_utils.bzl", "find_sysroot") +load("//rust/private:utils.bzl", "find_cc_toolchain", "find_toolchain", "get_lib_name", "get_preferred_artifact") +load("//util/launcher:launcher.bzl", "create_launcher") + +def get_cc_compile_env(cc_toolchain, feature_configuration): + """Gather cc environment variables from the given `cc_toolchain` + + Args: + cc_toolchain (cc_toolchain): The current rule's `cc_toolchain`. + feature_configuration (FeatureConfiguration): Class used to construct command lines from CROSSTOOL features. + + Returns: + dict: Returns environment variables to be set for given action. + """ + compile_variables = cc_common.create_compile_variables( + feature_configuration = feature_configuration, + cc_toolchain = cc_toolchain, + ) + return cc_common.get_environment_variables( + feature_configuration = feature_configuration, + action_name = C_COMPILE_ACTION_NAME, + variables = compile_variables, + ) def _rust_doc_test_impl(ctx): """The implementation for the `rust_doc_test` rule @@ -41,11 +64,7 @@ def _rust_doc_test_impl(ctx): # Construct rustdoc test command, which will be written to a shell script # to be executed to run the test. - flags = _build_rustdoc_flags(dep_info, crate_info) - if toolchain.os != "windows": - rust_doc_test = _build_rustdoc_test_bash_script(ctx, toolchain, flags, crate_info) - else: - rust_doc_test = _build_rustdoc_test_batch_script(ctx, toolchain, flags, crate_info) + flags, env, toolchain_tools = _build_rustdoc_flags(ctx, dep_info, crate_info, toolchain) # The test script compiles the crate and runs it, so it needs both compile and runtime inputs. compile_inputs = depset( @@ -58,16 +77,31 @@ def _rust_doc_test_impl(ctx): dep_info.transitive_libs, toolchain.rustc_lib.files, toolchain.rust_lib.files, - ], + ] + toolchain_tools, ) - return [DefaultInfo( - runfiles = ctx.runfiles( - files = compile_inputs.to_list(), - collect_data = True, - ), - executable = rust_doc_test, - )] + rustdoc = ctx.actions.declare_file(ctx.label.name + toolchain.binary_ext) + ctx.actions.symlink( + output = rustdoc, + target_file = toolchain.rust_doc, + is_executable = True, + ) + + return create_launcher( + ctx = ctx, + args = [ + "--test", + crate_info.root.path, + "--crate-name={}".format(crate_info.name), + ] + flags, + env = env, + toolchain = toolchain, + providers = [DefaultInfo( + files = toolchain.rustc_lib.files, + runfiles = ctx.runfiles(transitive_files = compile_inputs), + )], + executable = rustdoc, + ) # TODO: Replace with bazel-skylib's `path.dirname`. This requires addressing some dependency issues or # generating docs will break. @@ -82,12 +116,14 @@ def _dirname(path_str): """ return "/".join(path_str.split("/")[:-1]) -def _build_rustdoc_flags(dep_info, crate_info): +def _build_rustdoc_flags(ctx, dep_info, crate_info, toolchain): """Constructs the rustdoc script used to test `crate`. Args: + ctx (ctx): The rule's context object. dep_info (DepInfo): The DepInfo provider crate_info (CrateInfo): The CrateInfo provider + toolchain (rust_toolchain): The curret `rust_toolchain`. Returns: list: A list of rustdoc flags (str) @@ -103,6 +139,11 @@ def _build_rustdoc_flags(dep_info, crate_info): link_flags += ["--extern=" + c.name + "=" + c.dep.output.short_path for c in d.direct_crates.to_list()] link_search_flags += ["-Ldependency={}".format(_dirname(c.output.short_path)) for c in d.transitive_crates.to_list()] + sysroot = find_sysroot(toolchain, short_path = True) + if sysroot: + link_search_flags.append("--sysroot=${{pwd}}/{}".format(sysroot)) + link_search_flags.append("--target={}".format(toolchain.target_flag_value)) + # TODO(hlopko): use the more robust logic from rustc.bzl also here, through a reasonable API. for lib_to_link in dep_info.transitive_noncrates.to_list(): is_static = bool(lib_to_link.static_library or lib_to_link.pic_static_library) @@ -119,81 +160,49 @@ def _build_rustdoc_flags(dep_info, crate_info): edition_flags = ["--edition={}".format(crate_info.edition)] if crate_info.edition != "2015" else [] - return link_search_flags + link_flags + edition_flags - -_rustdoc_test_bash_script = """\ -#!/usr/bin/env bash - -set -e; - -{rust_doc} --test \\ - {crate_root} \\ - --crate-name={crate_name} \\ - {flags} -""" - -def _build_rustdoc_test_bash_script(ctx, toolchain, flags, crate_info): - """Generates a helper script for executing a rustdoc test for unix systems - - Args: - ctx (ctx): The `rust_doc_test` rule's context object - toolchain (ToolchainInfo): A rustdoc toolchain - flags (list): A list of rustdoc flags (str) - crate_info (CrateInfo): The CrateInfo provider - - Returns: - File: An executable containing information for a rustdoc test - """ - rust_doc_test = ctx.actions.declare_file( - ctx.label.name + ".sh", - ) - ctx.actions.write( - output = rust_doc_test, - content = _rustdoc_test_bash_script.format( - rust_doc = toolchain.rust_doc.short_path, - crate_root = crate_info.root.path, - crate_name = crate_info.name, - # TODO: Should be possible to do this with ctx.actions.Args, but can't seem to get them as a str and into the template. - flags = " \\\n ".join(flags), - ), - is_executable = True, - ) - return rust_doc_test - -_rustdoc_test_batch_script = """\ -{rust_doc} --test ^ - {crate_root} ^ - --crate-name={crate_name} ^ - {flags} -""" - -def _build_rustdoc_test_batch_script(ctx, toolchain, flags, crate_info): - """Generates a helper script for executing a rustdoc test for windows systems - - Args: - ctx (ctx): The `rust_doc_test` rule's context object - toolchain (ToolchainInfo): A rustdoc toolchain - flags (list): A list of rustdoc flags (str) - crate_info (CrateInfo): The CrateInfo provider - - Returns: - File: An executable containing information for a rustdoc test - """ - rust_doc_test = ctx.actions.declare_file( - ctx.label.name + ".bat", - ) - ctx.actions.write( - output = rust_doc_test, - content = _rustdoc_test_batch_script.format( - rust_doc = toolchain.rust_doc.short_path.replace("/", "\\"), - crate_root = crate_info.root.path, - crate_name = crate_info.name, - # TODO: Should be possible to do this with ctx.actions.Args, but can't seem to get them as a str and into the template. - flags = " ^\n ".join(flags), - ), - is_executable = True, - ) - return rust_doc_test + flags = link_search_flags + link_flags + edition_flags + + # Start with the default shell env, which contains any --action_env + # settings passed in on the command line. + env = dict(ctx.configuration.default_shell_env) + + # Build a set of environment variables to use for the test + toolchain_tools = [] + cc_toolchain, feature_configuration = find_cc_toolchain(ctx) + cc_env = get_cc_compile_env(cc_toolchain, feature_configuration) + print(cc_env) + + # MSVC requires INCLUDE to be set + include = cc_env.get("INCLUDE") + if include: + env["INCLUDE"] = include + + if cc_toolchain: + toolchain_tools.append(cc_toolchain.all_files) + + cc_executable = cc_toolchain.compiler_executable + if cc_executable: + env["CC"] = cc_executable + ar_executable = cc_toolchain.ar_executable + if ar_executable: + # The default MacOS toolchain uses libtool as ar_executable not ar. + # This doesn't work when used as $AR, so simply don't set it - tools will probably fall back to + # /usr/bin/ar which is probably good enough. + if not ar_executable.endswith("libtool"): + env["AR"] = ar_executable + flags.extend(["-C", "linker={}".format(ar_executable)]) + if cc_toolchain.sysroot: + env["SYSROOT"] = cc_toolchain.sysroot + + # Gets the paths to the folders containing the standard library (or libcore) + rust_lib_files = depset(transitive = [toolchain.rust_lib.files, toolchain.rustc_lib.files]) + rust_lib_paths = depset([file.short_path for file in rust_lib_files.to_list()]).to_list() + rust_lib_dirs = depset([file.rsplit("/", 1)[0] for file in rust_lib_paths]).to_list() + rust_lib_dirs = ["${{pwd}}/{}".format(lib) for lib in rust_lib_dirs] + # env["SYSROOT"] = "${{pwd}}/{}".format(sysroot) + env["LD_LIBRARY_PATH"] = env.get("LD_LIBRARY_PATH", "") + ":".join(rust_lib_dirs) + env["DYLD_LIBRARY_PATH"] = env.get("DYLD_LIBRARY_PATH", "") + ":".join(rust_lib_dirs) + return flags, env, toolchain_tools rust_doc_test = rule( implementation = _rust_doc_test_impl, @@ -212,10 +221,25 @@ rust_doc_test = rule( doc = "__deprecated__: use `crate`", providers = [rust_common.crate_info], ), + "_cc_toolchain": attr.label( + default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"), + ), + "_launcher": attr.label( + executable = True, + default = Label("//util/launcher:launcher"), + cfg = "exec", + doc = ( + "A launcher executable for loading environment and argument files passed in via the " + + "`env` attribute and ensuring the variables are set for the underlying test executable." + ), + ), }, - executable = True, test = True, - toolchains = [str(Label("//rust:toolchain"))], + fragments = ["cpp"], + toolchains = [ + str(Label("//rust:toolchain")), + "@bazel_tools//tools/cpp:toolchain_type", + ], incompatible_use_toolchain_transition = True, doc = """Runs Rust documentation tests. diff --git a/rust/private/toolchain_utils.bzl b/rust/private/toolchain_utils.bzl index f26acf298b..99095e7143 100644 --- a/rust/private/toolchain_utils.bzl +++ b/rust/private/toolchain_utils.bzl @@ -1,5 +1,31 @@ """A module defining toolchain utilities""" +def find_sysroot(rust_toolchain, short_path = False): + """Locate the sysroot for a given toolchain + + Args: + rust_toolchain (rust_toolchain): A rust toolchain + short_path (bool): Whether or not to use a short path to the sysroot + + Returns: + str, optional: The path of the toolchain's sysroot + """ + + # Sysroot is determined by using a rust stdlib file, expected to be at + # `${SYSROOT}/lib/rustlib/${target_triple}/lib`, and strip the known + # directories from the sysroot path. + rust_stdlib_files = rust_toolchain.rust_lib.files.to_list() + if rust_stdlib_files: + # Determine the sysroot by taking a rust stdlib file, expected to be `${sysroot}/lib` + if short_path: + split = rust_stdlib_files[0].short_path.rsplit("/", 5) + else: + split = rust_stdlib_files[0].path.rsplit("/", 5) + sysroot = split[0] + return sysroot + + return None + def _toolchain_files_impl(ctx): toolchain = ctx.toolchains[str(Label("//rust:toolchain"))] diff --git a/util/launcher/BUILD.bazel b/util/launcher/BUILD.bazel index 8b52057ea6..c3c0139749 100644 --- a/util/launcher/BUILD.bazel +++ b/util/launcher/BUILD.bazel @@ -1,3 +1,4 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") load("//rust:rust.bzl", "rust_binary") package(default_visibility = ["//visibility:public"]) @@ -7,3 +8,10 @@ rust_binary( srcs = ["launcher_main.rs"], edition = "2018", ) + +bzl_library( + name = "bzl_lib", + srcs = glob( + ["**/*.bzl"], + ), +) diff --git a/util/launcher/launcher.bzl b/util/launcher/launcher.bzl new file mode 100644 index 0000000000..b56943213b --- /dev/null +++ b/util/launcher/launcher.bzl @@ -0,0 +1,158 @@ +"""Rust executable launcher module""" + +# buildifier: disable=bzl-visibility +load("//rust/private:utils.bzl", "expand_dict_value_locations") + +def _write_environ(ctx, launcher_filename, env, data): + file = ctx.actions.declare_file(launcher_filename + ".launchfiles/env") + environ = expand_dict_value_locations( + ctx, + env, + data, + ) + + # Convert the environment variables into a list to be written into a file. + environ_list = [] + for key, value in sorted(environ.items()): + environ_list.extend([key, value]) + + ctx.actions.write( + output = file, + content = "\n".join(environ_list), + ) + + return file + +def _write_args(ctx, launcher_filename, args, data): + # Convert the arguments list into a dictionary so args can benefit from + # the existing expand_dict_value_locations functionality + args_dict = {"{}".format(i): args[i] for i in range(0, len(args))} + + file = ctx.actions.declare_file(launcher_filename + ".launchfiles/args") + expanded_args = expand_dict_value_locations( + ctx, + args_dict, + data, + ) + + ctx.actions.write( + output = file, + content = "\n".join(expanded_args.values()), + ) + + return file + +def _write_executable(ctx, launcher_filename, executable = None): + file = ctx.actions.declare_file(launcher_filename + ".launchfiles/exec") + + ctx.actions.write( + output = file, + content = executable.path if executable else "", + ) + + return file + +def _merge_providers(ctx, providers, launcher, launcher_files, executable = None): + # Replace the `DefaultInfo` provider in the returned list + default_info = None + for i in range(len(providers)): + if type(providers[i]) == "DefaultInfo": + default_info = providers[i] + providers.pop(i) + break + + if not default_info: + fail("list must contain a `DefaultInfo` provider") + + # Additionally update the `OutputGroupInfo` provider + output_group_info = None + for i in range(len(providers)): + if type(providers[i]) == "OutputGroupInfo": + output_group_info = providers[i] + providers.pop(i) + break + + if output_group_info: + output_group_info = OutputGroupInfo( + launcher_files = depset(launcher_files), + output = depset([executable or default_info.files_to_run.executable]), + **output_group_info + ) + else: + output_group_info = OutputGroupInfo( + launcher_files = depset(launcher_files), + output = depset([executable or default_info.files_to_run.executable]), + ) + + # buildifier: disable=print + # print(ctx.label, default_info.default_runfiles.merge( + # # The original executable is now also considered a runfile + # ctx.runfiles(files = launcher_files + [ + # executable or default_info.files_to_run.executable, + # ]), + # ).files) + + providers.extend([ + DefaultInfo( + files = default_info.files, + runfiles = default_info.default_runfiles.merge( + # The original executable is now also considered a runfile + ctx.runfiles(files = launcher_files + [ + executable or default_info.files_to_run.executable, + ]), + ), + executable = launcher, + ), + output_group_info, + ]) + + return providers + +def create_launcher(ctx, toolchain, args = [], env = {}, data = [], providers = [], executable = None): + """Create a process wrapper to ensure runtime environment variables are defined for the test binary + + Args: + ctx (ctx): The rule's context object + toolchain (rust_toolchain): The current rust toolchain + args (list, optional): Optional arguments to include in the lancher + env (dict, optional): Optional environment variables to include in the lancher + data (list, optional): Targets to use when performing location expansion on `args` and `env`. + providers (list, optional): Providers from a rust compile action. See `rustc_compile_action` + executable (File, optional): An optional executable for the launcher to wrap + + Returns: + list: A list of providers similar to `rustc_compile_action` but with modified default info + """ + + # TODO: It's unclear if the toolchain is in the same configuration as the `_launcher` attribute + # This should be investigated but for now, we generally assume if the target environment is windows, + # the execution environment is windows. + if toolchain.os == "windows": + launcher_filename = ctx.label.name + ".launcher.exe" + else: + launcher_filename = ctx.label.name + ".launcher" + + launcher = ctx.actions.declare_file(launcher_filename) + + # Because returned executables must be created from the same rule, the + # launcher target is simply symlinked and exposed. + ctx.actions.symlink( + output = launcher, + target_file = ctx.executable._launcher, + is_executable = True, + ) + + # Expand the environment variables and write them to a file + launcher_files = [ + _write_environ(ctx, launcher_filename, env, data), + _write_args(ctx, launcher_filename, args, data), + _write_executable(ctx, launcher_filename, executable), + ] + + return _merge_providers( + ctx = ctx, + providers = providers, + launcher = launcher, + launcher_files = launcher_files, + executable = executable, + ) diff --git a/util/launcher/launcher_main.rs b/util/launcher/launcher_main.rs index 006d051911..3aacfe3c87 100644 --- a/util/launcher/launcher_main.rs +++ b/util/launcher/launcher_main.rs @@ -11,6 +11,9 @@ use std::os::unix::process::CommandExt; /// This string must match the one found in `_create_test_launcher` const LAUNCHFILES_ENV_PATH: &str = ".launchfiles/env"; +/// This string must match the one found in `_create_test_launcher` +const LAUNCHFILES_ARGS_PATH: &str = ".launchfiles/args"; + /// Load environment variables from a uniquly formatted fn environ() -> BTreeMap { let mut environ = BTreeMap::new(); @@ -48,9 +51,10 @@ fn environ() -> BTreeMap { environ } -/// Locate the executable based on the name of the launcher executable +/// Locate the underlying executable intended to be started under the launcher fn executable() -> PathBuf { - // Consume the first argument (argv[0]) + // When no executable is provided explicitly via the launcher file, fallback + // to searching for the executable based on the name of the launcher itself. let mut exec_path = std::env::args().next().expect("arg 0 was not set"); let stem_index = exec_path .rfind(".launcher") @@ -67,12 +71,54 @@ fn executable() -> PathBuf { /// Parse the command line arguments but skip the first element which /// is the path to the test runner executable. fn args() -> Vec { - std::env::args().skip(1).collect() + // Load the environment file into a map + let args_path = std::env::args().next().expect("arg 0 was not set") + LAUNCHFILES_ARGS_PATH; + let file = File::open(args_path).expect("Failed to load the environment file"); + + // Variables will have the `${pwd}` variable replaced which is rendered by + // `@rules_rust//rust/private:util.bzl::expand_dict_value_locations` + let pwd = std::env::current_dir().expect("Failed to get current working directory"); + let pwd_str = pwd.to_string_lossy(); + + // Note that arguments from the args file always go first + BufReader::new(file) + .lines() + .map(|line| { + line.expect("Failed to read file") + .replace("${pwd}", &pwd_str) + }) + .chain(std::env::args().skip(1)) + .collect() +} + +fn print_tree(start: &str) { + println!("Name: {}", start); + for path in std::fs::read_dir(&start).unwrap() { + let path_buf = path.unwrap().path(); + if path_buf.is_file() { + println!("Name: {}", path_buf.display()); + } + } + for path in std::fs::read_dir(&start).unwrap() { + let path_buf = path.unwrap().path(); + if path_buf.is_dir() { + print_tree(path_buf.to_str().unwrap()); + } + } } /// Simply replace the current process with our test #[cfg(target_family = "unix")] fn exec(environ: BTreeMap, executable: PathBuf, args: Vec) { + println!("{:?}", std::env::current_dir()); + println!("{:#?}", args); + println!("{:#?}", environ); + println!("{:?}", executable); + + if environ.contains_key("SYSROOT") { + print_tree(&environ["SYSROOT"]); + } + let error = Command::new(&executable) .envs(environ.iter()) .args(args) @@ -85,13 +131,21 @@ fn exec(environ: BTreeMap, executable: PathBuf, args: Vec, executable: PathBuf, args: Vec) { - let output = Command::new(executable) + println!("{:?}", std::env::current_dir()); + println!("{:#?}", args); + println!("{:#?}", environ); + println!("{:?}", executable); + if environ.contains_key("SYSROOT") { + print_tree(&environ["SYSROOT"]); + } + let result = Command::new(executable) .envs(environ.iter()) .args(args) - .output() + .status() .expect("Failed to run process"); - std::process::exit(output.status.code().unwrap_or(1)); + println!("Done: {:?}", result.code().unwrap_or(1234)); + std::process::exit(result.code().unwrap_or(1)); } /// Main entrypoint