Skip to content

Commit cad093a

Browse files
committed
Make commands cuter
This is a different approach from the previous commit. I zoomed out and considered the over all problem space: ## Desires: - A representation of the command being run should appear in logs, or the error, or both - Stdout and Stderr from the command should appear in logs, or the error, but NOT both (if it runs). - Make non-zero status an error - Allow for debugging a command that was run after execution (i.e. get command program name, arguments, env, cwd and pass them to something like `WhichProblem`) - Ideally only have to format `Command` once ## Central std structs: - `Command` holds all state needed to run a process. Must be `mut` to be run. `&mut` references are exclusive and can be tricky to work with. Cannot be cloned. - `Output` holds stdout, stderr, and status if the command could be executed. - `std::io::Error` holds failure information if the process could not boot (for example, could not find the executable) ## Consequences - Command must be represented a minimum of 3 times for logging: - For logging - In the io error state - In the Ok state so it can be converted into an Err on invalid status - Another time if we need to access it for debugging i.e. WhichProblem - We need both a Command representation and Output data at the same time - Generating an `Output` requires a mutable reference to the command. - We need both a Command representation and std::io::Err at the same time ## Implementation I realized that I wanted a flow of data - Command - Command w/Name - Command w/Name & Result<Output, std::io::Error> This gives us a way to build up a canonical representation of the command as a "name". I chose to implement these three states as structs with functions that allow for transition between structs. I also need to record if the contents are streamed or not. For this I implemented an enum, but users don't need to see it if they only ever call helper methods like: `stream()` or `run-quietly()`. Once the final state is achieved, it's still not very ergonomic, so converted it into an enum instead that represents the possible states of the system: - Command ran, status = 0 - NonZero status, stderr is not streamed - NonZero status, stderr is already streamed - IO Error If someone wants to take this and use it, they can. It would be very easy to map non-zero status back to an `Ok` state as they share the same inner struct type. However that enum cannot be used as an Error as it contains `& mut Command` with a lifetime which makes it not terribly useful. I provided a default error enum and an easy way to map that status enum into an error via a method. This allows the end user to be able to chain all `cute_cmd` calls and then to `map` or `map_err` if they need more control. ## TODO Add `which_problem` integration back in. More docs. Get a review.
1 parent 3113e67 commit cad093a

File tree

5 files changed

+366
-385
lines changed

5 files changed

+366
-385
lines changed

buildpacks/ruby/src/layers/bundle_install_layer.rs

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use crate::{BundleWithout, RubyBuildpack, RubyBuildpackError};
22
use commons::{
3-
cmd, cmd::CmdError, display::SentenceList, gemfile_lock::ResolvedRubyVersion,
3+
cute_cmd::{self, cute_name_with_env, CuteCmd, CuteCmdError},
4+
display::SentenceList,
5+
gemfile_lock::ResolvedRubyVersion,
46
metadata_digest::MetadataDigest,
57
};
68
use libcnb::{
@@ -10,7 +12,6 @@ use libcnb::{
1012
layer_env::{LayerEnv, ModificationBehavior, Scope},
1113
Env,
1214
};
13-
use libherokubuildpack::command::CommandExt;
1415
use libherokubuildpack::log as user;
1516
use serde::{Deserialize, Serialize};
1617
use std::path::Path;
@@ -337,32 +338,27 @@ fn layer_env(layer_path: &Path, app_dir: &Path, without_default: &BundleWithout)
337338
///
338339
/// When the 'bundle install' command fails this function returns an error.
339340
///
340-
fn bundle_install(env: &Env) -> Result<(), CmdError> {
341+
fn bundle_install(env: &Env) -> Result<(), CuteCmdError> {
341342
// ## Run `$ bundle install`
342343

343-
let bundle = "bundle";
344-
let mut command = cmd::create(bundle, &["install"], env);
345-
let command_string = cmd::display_with_keys(
346-
&command,
347-
env,
348-
[
349-
"BUNDLE_BIN",
350-
"BUNDLE_CLEAN",
351-
"BUNDLE_DEPLOYMENT",
352-
"BUNDLE_GEMFILE",
353-
"BUNDLE_PATH",
354-
"BUNDLE_WITHOUT",
355-
],
356-
);
357-
358-
user::log_info(format!("\nRunning command:\n$ {command_string}"));
359-
360-
command
361-
.output_and_write_streams(std::io::stdout(), std::io::stderr())
362-
.map_err(|error| cmd::annotate_which_problem(error, bundle.into(), env.get("PATH")))
363-
.map_err(cmd::os_command_error)
364-
.and_then(|output| cmd::check_non_zero(output, cmd::OutputState::AlreadyStreamed))
365-
.map_err(|error| CmdError::new(command_string, error))?;
344+
let mut command = cute_cmd::plain("bundle", &["install"], env);
345+
346+
CuteCmd::new_with(&mut command, |command| {
347+
cute_name_with_env(
348+
command,
349+
[
350+
"BUNDLE_BIN",
351+
"BUNDLE_CLEAN",
352+
"BUNDLE_DEPLOYMENT",
353+
"BUNDLE_GEMFILE",
354+
"BUNDLE_PATH",
355+
"BUNDLE_WITHOUT",
356+
],
357+
)
358+
})
359+
.hello(|name| user::log_info(format!("\nRunning command:\n$ {name}")))
360+
.stream()
361+
.to_result()?;
366362

367363
Ok(())
368364
}

buildpacks/ruby/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
#![allow(clippy::module_name_repetitions)]
44
use crate::layers::{RubyInstallError, RubyInstallLayer};
55
use commons::cache::CacheError;
6-
use commons::cmd::CmdError;
6+
use commons::cute_cmd::CuteCmdError;
77
use commons::env_command::CommandError;
88
use commons::gem_list::GemList;
99
use commons::gemfile_lock::GemfileLock;
@@ -170,7 +170,7 @@ pub(crate) enum RubyBuildpackError {
170170
MissingGemfileLock(std::io::Error),
171171
InAppDirCacheError(CacheError),
172172
BundleInstallDigestError(commons::metadata_digest::DigestError),
173-
BundleInstallCommandError(CmdError),
173+
BundleInstallCommandError(CuteCmdError),
174174
RakeAssetsPrecompileFailed(CommandError),
175175
GemInstallBundlerCommandError(CommandError),
176176
}

0 commit comments

Comments
 (0)