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

Use unified Heroku buildpack output style #745

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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,71 @@ jobs:
working-directory: ${{ matrix.buildpack-directory }}
# Runs only tests annotated with the `ignore` attribute (which in this repo, are the integration tests).
run: cargo test --locked -- --ignored --test-threads 16

print-pack-getting-started-output:
runs-on: ${{ matrix.target == 'aarch64-unknown-linux-musl' && 'pub-hk-ubuntu-24.04-arm-medium' || 'ubuntu-24.04' }}
strategy:
matrix:
target: ["aarch64-unknown-linux-musl", "x86_64-unknown-linux-musl"]
guide: ["heroku/java-getting-started", "heroku/gradle-getting-started", "heroku/scala-getting-started"]
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Install musl-tools
run: sudo apt-get install -y --no-install-recommends musl-tools
- name: Update Rust toolchain
run: rustup update
- name: Install Rust linux-musl target
run: rustup target add ${{ matrix.target }}
- name: Rust Cache
uses: Swatinem/[email protected]
- name: Install Pack CLI
uses: buildpacks/github-actions/[email protected]
- name: Pull builder and run images
run: |
docker pull "heroku/builder:24"
docker pull "heroku/heroku:24"
- name: Clone getting started guide
uses: actions/checkout@v4
with:
repository: ${{ matrix.guide }}
path: tmp/guide
- name: Install libcnb-cargo for `cargo libcnb package` command
run: cargo install libcnb-cargo
- name: Compile buildpack
run: cargo libcnb package --target ${{ matrix.target }}
- name: "PRINT: Getting started guide output"
run: |
set -euo pipefail

PACK_CMD="pack build my-image --force-color --builder heroku/builder:24 --trust-extra-buildpacks --path tmp/guide --pull-policy never "
case "${{ matrix.guide }}" in
"heroku/java-getting-started")
PACK_CMD+=" --buildpack packaged/${{ matrix.target }}/debug/heroku_jvm "
PACK_CMD+=" --buildpack packaged/${{ matrix.target }}/debug/heroku_java "
PACK_CMD+=" --buildpack packaged/${{ matrix.target }}/debug/heroku_maven "
;;
"heroku/gradle-getting-started")
PACK_CMD+=" --buildpack packaged/${{ matrix.target }}/debug/heroku_jvm "
PACK_CMD+=" --buildpack packaged/${{ matrix.target }}/debug/heroku_gradle "
;;
"heroku/scala-getting-started")
PACK_CMD+=" --buildpack packaged/${{ matrix.target }}/debug/heroku_jvm "
PACK_CMD+=" --buildpack packaged/${{ matrix.target }}/debug/heroku_sbt "
PACK_CMD+=" --buildpack packaged/${{ matrix.target }}/debug/heroku_scala "
;;
*)
echo "Unknown guide: ${{ matrix.guide }}"
exit 1
;;
esac

echo "Running command: $PACK_CMD"
bash -c "$PACK_CMD"
echo ""
echo "With CACHE example"
echo ""
bash -c "$PACK_CMD"
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ This buildpack relies on [heroku/libcnb.rs][libcnb] to compile buildpacks. All
[libcnb.rs dependencies][libcnb-deps] will need to be setup prior to building
or testing this buildpack.

### Clone

This project uses submodules, to clone all code run:

```
$ git clone --recursive https://github.com/heroku/buildpacks-jvm
```

### Building

1. Run `cargo check` to download dependencies and ensure there are no
Expand Down
2 changes: 1 addition & 1 deletion buildpacks/gradle/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ workspace = true
buildpacks-jvm-shared.workspace = true
indoc = "2"
libcnb = "=0.26.0"
libherokubuildpack = { version = "=0.26.0", default-features = false, features = ["command", "error", "log"] }
libherokubuildpack = { version = "=0.26.0", default-features = false, features = ["command", "error"] }
nom = "7"
serde = { version = "1", features = ["derive"] }

Expand Down
71 changes: 39 additions & 32 deletions buildpacks/gradle/src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
use crate::GradleBuildpackError;
use buildpacks_jvm_shared::log::{
log_build_tool_io_error, log_build_tool_unexpected_exit_code_error, log_please_try_again_error,
};
use buildpacks_jvm_shared as shared;
use indoc::indoc;
use libherokubuildpack::log::log_error;

#[allow(clippy::too_many_lines, clippy::needless_pass_by_value)]
pub(crate) fn on_error_gradle_buildpack(error: GradleBuildpackError) {
match error {
GradleBuildpackError::GradleWrapperNotFound => {
log_error(
shared::output::print_error(
"Missing Gradle Wrapper",
indoc! {"
This buildpack leverages Gradle Wrapper to install the correct Gradle version to build your application.
Expand All @@ -23,43 +20,53 @@ pub(crate) fn on_error_gradle_buildpack(error: GradleBuildpackError) {
"},
);
}
GradleBuildpackError::GradleBuildIoError(error) => log_build_tool_io_error("Gradle", error),
GradleBuildpackError::GradleBuildIoError(error) => {
shared::log::log_build_tool_io_error("Gradle", error);
}
GradleBuildpackError::GradleBuildUnexpectedStatusError(exit_status) => {
log_build_tool_unexpected_exit_code_error("Gradle", exit_status);
shared::log::log_build_tool_unexpected_exit_code_error("Gradle", exit_status);
}
GradleBuildpackError::GetTasksError(error) => log_please_try_again_error(
GradleBuildpackError::GetTasksError(error) => shared::log::log_please_try_again_error(
"Failed to get Gradle tasks",
"Failed to get Gradle tasks",
error,
),
GradleBuildpackError::GetDependencyReportError(error) => log_please_try_again_error(
"Failed to get Gradle dependency report",
"Failed to get Gradle dependency report",
error,
),
GradleBuildpackError::WriteGradlePropertiesError(error) => log_please_try_again_error(
"Failed to write Gradle configuration",
"Failed to write Gradle configuration",
error,
),
GradleBuildpackError::WriteGradleInitScriptError(error) => log_please_try_again_error(
"Failed to write Gradle init script",
"Failed to write Gradle init script",
error,
),
GradleBuildpackError::GetDependencyReportError(error) => {
shared::log::log_please_try_again_error(
"Failed to get Gradle dependency report",
"Failed to get Gradle dependency report",
error,
);
}
GradleBuildpackError::WriteGradlePropertiesError(error) => {
shared::log::log_please_try_again_error(
"Failed to write Gradle configuration",
"Failed to write Gradle configuration",
error,
);
}
GradleBuildpackError::WriteGradleInitScriptError(error) => {
shared::log::log_please_try_again_error(
"Failed to write Gradle init script",
"Failed to write Gradle init script",
error,
);
}
GradleBuildpackError::CannotSetGradleWrapperExecutableBit(error) => {
log_please_try_again_error(
shared::log::log_please_try_again_error(
"Failed to set executable bit for Gradle wrapper",
"Failed to set executable bit for Gradle wrapper",
error,
);
}
GradleBuildpackError::StartGradleDaemonError(error) => log_please_try_again_error(
"Failed to start Gradle daemon",
"The Gradle daemon for this build could not be started.",
error,
),
GradleBuildpackError::BuildTaskUnknown => log_error(
GradleBuildpackError::StartGradleDaemonError(error) => {
shared::log::log_please_try_again_error(
"Failed to start Gradle daemon",
"The Gradle daemon for this build could not be started.",
error,
);
}
GradleBuildpackError::BuildTaskUnknown => shared::output::print_error(
"Failed to determine build task",
indoc! {"
It looks like your project does not contain a 'stage' task, which Heroku needs in order
Expand All @@ -69,14 +76,14 @@ pub(crate) fn on_error_gradle_buildpack(error: GradleBuildpackError) {
"},
),
GradleBuildpackError::DetectError(error) => {
log_please_try_again_error(
shared::log::log_please_try_again_error(
"Failed to determine if a file exists during detect",
"Failed to determine if a file exists during detect",
error,
);
}
GradleBuildpackError::CannotDetermineDefaultAppProcess(error) => {
log_please_try_again_error(
shared::log::log_please_try_again_error(
"Failed to determine default app process",
"Failed to determine default app process",
error,
Expand Down
5 changes: 2 additions & 3 deletions buildpacks/gradle/src/gradle_command/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pub(crate) fn start(
) -> Result<(), GradleCommandError<()>> {
let output = Command::new(gradle_wrapper_executable_path)
.args([
// Fixes an issue when when running under Apple Rosetta emulation
// Fixes an issue when running under Apple Rosetta emulation
"-Djdk.lang.Process.launchMechanism=vfork",
"--daemon",
GRADLE_TASK_NAME_HEROKU_START_DAEMON,
Expand All @@ -39,8 +39,7 @@ pub(crate) fn stop(
Command::new(gradle_wrapper_executable_path)
.args(["-q", "--stop"])
.envs(gradle_env)
.spawn()
.and_then(|mut child| child.wait())
.output()
.map_err(GradleCommandError::Io)?;

Ok(())
Expand Down
145 changes: 81 additions & 64 deletions buildpacks/gradle/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ use crate::gradle_command::GradleCommandError;
use crate::layers::gradle_home::handle_gradle_home_layer;
use crate::GradleBuildpackError::{GradleBuildIoError, GradleBuildUnexpectedStatusError};
use buildpacks_jvm_shared as shared;
use buildpacks_jvm_shared::output::{
print_buildpack_name, print_section, print_subsection, track_buildpack_timing,
track_subsection_timing, BuildpackOutputText, BuildpackOutputTextSection,
};
#[cfg(test)]
use buildpacks_jvm_shared_test as _;
use libcnb::build::{BuildContext, BuildResult, BuildResultBuilder};
Expand All @@ -16,10 +20,7 @@ use libcnb::generic::GenericPlatform;
use libcnb::{buildpack_main, Buildpack, Env};
#[cfg(test)]
use libcnb_test as _;
use libherokubuildpack::command::CommandExt;
use libherokubuildpack::log::log_header;
use serde::Deserialize;
use std::io::{stderr, stdout};
use std::process::{Command, ExitStatus};

mod config;
Expand Down Expand Up @@ -75,68 +76,84 @@ impl Buildpack for GradleBuildpack {
}

fn build(&self, context: BuildContext<Self>) -> libcnb::Result<BuildResult, Self::Error> {
log_header("Gradle Buildpack");
let buildpack_config = GradleBuildpackConfig::from(&context);

let gradle_wrapper_executable_path = Some(context.app_dir.join("gradlew"))
.filter(|path| path.exists())
.ok_or(GradleBuildpackError::GradleWrapperNotFound)?;

shared::fs::set_executable(&gradle_wrapper_executable_path)
.map_err(GradleBuildpackError::CannotSetGradleWrapperExecutableBit)?;

let mut gradle_env = Env::from_current();
handle_gradle_home_layer(&context, &mut gradle_env)?;

log_header("Starting Gradle Daemon");
gradle_command::start_daemon(&gradle_wrapper_executable_path, &gradle_env)
.map_err(GradleBuildpackError::StartGradleDaemonError)?;

let project_tasks = gradle_command::tasks(&context.app_dir, &gradle_env)
.map_err(|command_error| command_error.map_parse_error(|_| ()))
.map_err(GradleBuildpackError::GetTasksError)?;

let dependency_report = gradle_command::dependency_report(&context.app_dir, &gradle_env)
.map_err(GradleBuildpackError::GetDependencyReportError)?;

let task_name = buildpack_config
.gradle_task
.as_deref()
.or_else(|| project_tasks.has_task("stage").then_some("stage"))
.or_else(|| {
detect_framework(&dependency_report).map(|framework| match framework {
Framework::SpringBoot | Framework::Quarkus => "build",
Framework::Ratpack => "installDist",
Framework::Micronaut => "shadowJar",
track_buildpack_timing(|| {
print_buildpack_name("Heroku Gradle Buildpack");

let buildpack_config = GradleBuildpackConfig::from(&context);

let gradle_wrapper_executable_path = Some(context.app_dir.join("gradlew"))
.filter(|path| path.exists())
.ok_or(GradleBuildpackError::GradleWrapperNotFound)?;

shared::fs::set_executable(&gradle_wrapper_executable_path)
.map_err(GradleBuildpackError::CannotSetGradleWrapperExecutableBit)?;

let mut gradle_env = Env::from_current();
handle_gradle_home_layer(&context, &mut gradle_env)?;

print_section("Running Gradle build");

track_subsection_timing(|| {
print_subsection("Starting Gradle daemon");
gradle_command::start_daemon(&gradle_wrapper_executable_path, &gradle_env)
.map_err(GradleBuildpackError::StartGradleDaemonError)
})?;

let project_tasks = track_subsection_timing(|| {
print_subsection("Querying tasks");
gradle_command::tasks(&context.app_dir, &gradle_env)
.map_err(|command_error| command_error.map_parse_error(|_| ()))
.map_err(GradleBuildpackError::GetTasksError)
})?;

let dependency_report = track_subsection_timing(|| {
print_subsection("Querying dependency report");
gradle_command::dependency_report(&context.app_dir, &gradle_env)
.map_err(GradleBuildpackError::GetDependencyReportError)
})?;

let task_name = buildpack_config
.gradle_task
.as_deref()
.or_else(|| project_tasks.has_task("stage").then_some("stage"))
.or_else(|| {
detect_framework(&dependency_report).map(|framework| match framework {
Framework::SpringBoot | Framework::Quarkus => "build",
Framework::Ratpack => "installDist",
Framework::Micronaut => "shadowJar",
})
})
})
.ok_or(GradleBuildpackError::BuildTaskUnknown)?;

log_header("Running build task");

let output = Command::new(&gradle_wrapper_executable_path)
.current_dir(&context.app_dir)
.envs(&gradle_env)
.args([task_name, "-x", "check"])
.output_and_write_streams(stdout(), stderr())
.map_err(GradleBuildIoError)?;

if !output.status.success() {
Err(GradleBuildUnexpectedStatusError(output.status))?;
}

// Explicitly ignoring the result. If the daemon cannot be stopped, that is not a build
// failure, nor can we recover from it in any way.
let _ = gradle_command::stop_daemon(&gradle_wrapper_executable_path, &gradle_env);

let process = default_app_process(&dependency_report, &context.app_dir)
.map_err(GradleBuildpackError::CannotDetermineDefaultAppProcess)?;

process
.map_or(BuildResultBuilder::new(), |process| {
BuildResultBuilder::new().launch(LaunchBuilder::new().process(process).build())
})
.build()
.ok_or(GradleBuildpackError::BuildTaskUnknown)?;

print_section("Running Gradle build");
print_subsection(BuildpackOutputText::new(vec![
BuildpackOutputTextSection::regular("Running "),
BuildpackOutputTextSection::command(format!("./gradlew {task_name} -x check")),
]));

let mut build_command = Command::new(&gradle_wrapper_executable_path);
build_command
.current_dir(&context.app_dir)
.envs(&gradle_env)
.args([task_name, "-x", "check"]);

shared::output::run_command(build_command, false, GradleBuildIoError, |output| {
GradleBuildUnexpectedStatusError(output.status)
})?;

// Explicitly ignoring the result. If the daemon cannot be stopped, that is not a build
// failure, nor can we recover from it in any way.
let _ = gradle_command::stop_daemon(&gradle_wrapper_executable_path, &gradle_env);

let process = default_app_process(&dependency_report, &context.app_dir)
.map_err(GradleBuildpackError::CannotDetermineDefaultAppProcess)?;

process
.map_or(BuildResultBuilder::new(), |process| {
BuildResultBuilder::new().launch(LaunchBuilder::new().process(process).build())
})
.build()
})
}

fn on_error(&self, error: libcnb::Error<Self::Error>) {
Expand Down
Loading