Skip to content

Commit

Permalink
Port build output from Ruby build pack
Browse files Browse the repository at this point in the history
This is a port of https://github.com/heroku/buildpacks-ruby/tree/016ee969647afc6aa9565ff3a44594e308f83380/commons/src/output from the Ruby build pack.

The goal is to provide a standard format for outputting build information to the end user while a buildpack is running.
  • Loading branch information
Richard Schneeman committed Nov 6, 2023
1 parent 5ba2781 commit 7ec07e1
Show file tree
Hide file tree
Showing 13 changed files with 2,175 additions and 1 deletion.
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,21 @@ jobs:
# TODO: Switch this back to using the `alpine` tag once the stable Pack CLI release supports
# image extensions (currently newer sample alpine images fail to build with stable Pack).
run: pack build example-basics --builder cnbs/sample-builder@sha256:da5ff69191919f1ff30d5e28859affff8e39f23038137c7751e24a42e919c1ab --trust-builder --buildpack packaged/x86_64-unknown-linux-musl/debug/libcnb-examples_basics --path examples/

print-style-guide:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install musl-tools
run: sudo apt-get install musl-tools --no-install-recommends
- name: Update Rust toolchain
run: rustup update
- name: Install Rust linux-musl target
run: rustup target add x86_64-unknown-linux-musl
- name: Rust Cache
uses: Swatinem/[email protected]
- name: Install Pack CLI
uses: buildpacks/github-actions/[email protected]
- name: Run integration tests
run: cargo run print_style_guide
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- `libherokubuildpack`:
- Added build `output` module. This will help buildpack authors provide consistent and delightful output to their buildpack users ([#721](https://github.com/heroku/libcnb.rs/pull/721))

## [0.15.0] - 2023-09-25

Expand Down
25 changes: 24 additions & 1 deletion libherokubuildpack/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,22 @@ include = ["src/**/*", "LICENSE", "README.md"]
[package.metadata.docs.rs]
all-features = true

[[example]]
name = "print_style_guide"

[features]
default = ["command", "download", "digest", "error", "log", "tar", "toml", "fs", "write"]
default = [
"command",
"download",
"digest",
"error",
"log",
"tar",
"toml",
"fs",
"write",
"output",
]
download = ["dep:ureq", "dep:thiserror"]
digest = ["dep:sha2"]
error = ["log", "dep:libcnb"]
Expand All @@ -24,6 +38,7 @@ tar = ["dep:tar", "dep:flate2"]
toml = ["dep:toml"]
fs = ["dep:pathdiff"]
command = ["write", "dep:crossbeam-utils"]
output = ["dep:lazy_static", "dep:regex", "dep:const_format"]
write = []

[dependencies]
Expand All @@ -42,6 +57,14 @@ termcolor = { version = "1.3.0", optional = true }
thiserror = { version = "1.0.48", optional = true }
toml = { workspace = true, optional = true }
ureq = { version = "2.7.1", default-features = false, features = ["tls"], optional = true }
lazy_static = { version = "1", optional = true }
regex = { version = "1", optional = true }
const_format = { version = "0.2", optional = true }

[dev-dependencies]
tempfile = "3.8.0"
libcnb-test = "0.15.0"
indoc = "2"
pretty_assertions = "1"
fun_run = "0.1.1"
ascii_table = { version = "4", features = ["color_codes"] }
205 changes: 205 additions & 0 deletions libherokubuildpack/examples/print_style_guide.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
use ascii_table::AsciiTable;
use fun_run::CommandWithName;
use libherokubuildpack::output::style::{self, DEBUG_INFO, HELP};
use libherokubuildpack::output::{
build_log::*,
section_log::{log_step, log_step_stream, log_step_timed},
};
use indoc::formatdoc;
use std::io::stdout;
use std::process::Command;

fn main() {
println!(
"{}",
formatdoc! {"
Living build output style guide
===============================
"}
);

{
let mut log = BuildLog::new(stdout()).buildpack_name("Section logging features");
log = log
.section("Section heading example")
.step("step example")
.step("step example two")
.end_section();

log = log
.section("Section and step description")
.step(
"A section should be a noun i.e. 'Ruby Version', consider this the section topic.",
)
.step("A step should be a verb i.e. 'Downloading'")
.step("Related verbs should be nested under a single section")
.step(
formatdoc! {"
Steps can be multiple lines long
However they're best as short, factual,
descriptions of what the program is doing.
"}
.trim(),
)
.step("Prefer a single line when possible")
.step("Sections and steps are sentence cased with no ending puncuation")
.step(&format!("{HELP} capitalize the first letter"))
.end_section();

let mut command = Command::new("bash");
command.args(["-c", "ps aux | grep cargo"]);

let mut stream = log.section("Timer steps")
.step("Long running code should execute with a timer printing to the UI, to indicate the progam did not hang.")
.step("Example:")
.step_timed("Background progress timer")
.finish_timed_step()
.step("Output can be streamed. Mostly from commands. Example:")
.step_timed_stream(&format!("Running {}", style::command(command.name())));

command.stream_output(stream.io(), stream.io()).unwrap();
log = stream.finish_timed_stream().end_section();
drop(log);
}

{
let mut log = BuildLog::new(stdout()).buildpack_name("Section log functions");
log = log
.section("Logging inside a layer")
.step(
formatdoc! {"
Layer interfaces are neither mutable nor consuming i.e.
```
fn create(
&self,
_context: &BuildContext<Self::Buildpack>,
layer_path: &Path,
) -> Result<LayerResult<Self::Metadata>, RubyBuildpackError>
```
To allow logging within a layer you can use the `output::section_log` interface.
"}
.trim_end(),
)
.step("This `section_log` inteface allows you to log without state")
.step("That means you're responsonsible creating a section before calling it")
.step("Here's an example")
.end_section();

let section_log = log.section("Example:");

log_step("log_step()");
log_step_timed("log_step_timed()", || {
// do work here
});
log_step_stream("log_step_stream()", |stream| {
Command::new("bash")
.args(["-c", "ps aux | grep cargo"])
.stream_output(stream.io(), stream.io())
.unwrap()
});
log_step(formatdoc! {"
If you want to help make sure you're within a section then you can require your layer
takes a reference to `&'a dyn SectionLogger`
"});
section_log.end_section();
}

{
let cmd_error = Command::new("iDoNotExist").named_output().err().unwrap();

let mut log = BuildLog::new(stdout()).buildpack_name("Error and warnings");
log = log
.section("Debug information")
.step("Should go above errors in section/step format")
.end_section();

log = log
.section(DEBUG_INFO)
.step(&cmd_error.to_string())
.end_section();

log.announce()
.warning(&formatdoc! {"
Warning: This is a warning header
This is a warning body. Warnings are for when we know for a fact a problem exists
but it's not bad enough to abort the build.
"})
.important(&formatdoc! {"
Important: This is important
Important is for when there's critical information that needs to be read
however it may or may not be a problem. If we know for a fact that there's
a problem then use a warning instead.
An example of something that is important but might not be a problem is
that an application owner upgraded to a new stack.
"})
.error(&formatdoc! {"
Error: This is an error header
This is the error body. Use an error for when the build cannot continue.
An error should include a header with a short description of why it cannot continue.
The body should include what error state was observed, why that's a problem, and
what remediation steps an application owner using the buildpack to deploy can
take to solve the issue.
"});
}

{
let mut log = BuildLog::new(stdout()).buildpack_name("Formatting helpers");

log = log
.section("The style module")
.step(&formatdoc! {"
Formatting helpers can be used to enhance log output:
"})
.end_section();

let mut table = AsciiTable::default();
table.set_max_width(240);
table.column(0).set_header("Example");
table.column(1).set_header("Code");
table.column(2).set_header("When to use");

let data: Vec<Vec<String>> = vec![
vec![
style::value("2.3.4"),
"style::value(\"2.3.f\")".to_string(),
"With versions, file names or other important values worth highlighting".to_string(),
],
vec![
style::url("https://www.schneems.com"),
"style::url(\"https://www.schneems.com\")".to_string(),
"With urls".to_string(),
],
vec![
style::command("bundle install"),
"style::command(command.name())".to_string(),
"With commands (alongside of `fun_run::CommandWithName`)".to_string(),
],
vec![
style::details("extra information"),
"style::details(\"extra information\")".to_string(),
"Add specific information at the end of a line i.e. 'Cache cleared (ruby version changed)'".to_string()
],
vec![
style::HELP.to_string(),
"style::HELP.to_string()".to_string(),
"A help prefix, use it in a step or section title".to_string()
],
vec![
style::DEBUG_INFO.to_string(),
"style::DEBUG_INFO.to_string()".to_string(),
"A debug prefix, use it in a step or section title".to_string()
]
];

table.print(data);
drop(log);
}
}
2 changes: 2 additions & 0 deletions libherokubuildpack/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ pub mod error;
pub mod fs;
#[cfg(feature = "log")]
pub mod log;
#[cfg(feature = "output")]
pub mod output;
#[cfg(feature = "tar")]
pub mod tar;
#[cfg(feature = "toml")]
Expand Down
Loading

0 comments on commit 7ec07e1

Please sign in to comment.