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

Allow making an executable-jar image by only providing an executable JAR #265

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

sap-ali
Copy link

@sap-ali sap-ali commented Apr 3, 2024

In this PR, I'm enabling the capability to only provide a standalone executable JAR without the need to have a file in META-INF/MANIFEST.MF

[Fixes #264]

@sap-ali sap-ali requested a review from a team as a code owner April 3, 2024 13:42
Copy link

linux-foundation-easycla bot commented Apr 3, 2024

CLA Signed

The committers listed above are authorized under a signed CLA.

@dmikusa
Copy link
Contributor

dmikusa commented Apr 3, 2024

Can you expand more on the use case here? I saw you other issue, but it's not clear to me exactly what you're trying to do and why. Can you provide some more details? A working reproduction including code/steps to produce a JAR file that fits what you're trying to do would be helpful.

@sap-ali
Copy link
Author

sap-ali commented Apr 3, 2024

Can you expand more on the use case here? I saw you other issue, but it's not clear to me exactly what you're trying to do and why. Can you provide some more details? A working reproduction including code/steps to produce a JAR file that fits what you're trying to do would be helpful.

Let's say we have build a fat JAR from our project: example-fat.jar.

We can apply the buildpack to create an image that runs the jar using:

pack build example:latest \
  --path example-fat.jar
  --builder paketobuildpacks/builder:base \
  --buildpack gcr.io/paketo-buildpacks/sap-machine \
  --buildpack gcr.io/paketo-buildpacks/syft \
  --buildpack gcr.io/paketo-buildpacks/executable-jar

However, this has the downside that the JAR is exploded in the image, which can cause some side-effects (as it has in our case using Quarkus).

Specifying --path . would also not work because there is no META-INF/MANIFEST.MF in the path.

Let's say we create such file identical to what already is there in the jar.

Now, we can build an image by running:

pack build example:latest \
  --path .
  --builder paketobuildpacks/builder:base \
  --buildpack gcr.io/paketo-buildpacks/sap-machine \
  --buildpack gcr.io/paketo-buildpacks/syft \
  --buildpack gcr.io/paketo-buildpacks/executable-jar

In this case, the buildpack is participating (because the META-INF/MANIFEST.MF exists) and can find the JAR by searching in the path. So, it can create a docker image in our desired way; an image with the exact jar that we had built before, not exploded or modified in any way.

However, now when we try to run the image we get a Class not found error for the defined entry point in the META-INF/MANIFEST.MF.

To fix this, first, we need to get rid of the requirement for a META-INF/MANIFEST.MF when we have a stand-alone JAR file that can run on its own. The rest of the code already handles the case very well. It is only in the detection phase, when it does not let the buildpack proceed without a manual META-INF/MANIFEST.MF.

With the proposed changes, when there is a stand-alone JAR in the path, the buildpack can find and place that JAR in the image without requiring a separate META-INF/MANIFEST.MF and without requiring to explode the JAR, and the produced image runs perfectly.

@sap-ali sap-ali changed the title Allow making an executable-jar image by only providing an executable JAR Allow making an executable-jar image by only providing an executable JAR [Fixes #264] Apr 3, 2024
@sap-ali sap-ali changed the title Allow making an executable-jar image by only providing an executable JAR [Fixes #264] Allow making an executable-jar image by only providing an executable JAR Apr 3, 2024
@sap-ali
Copy link
Author

sap-ali commented Apr 4, 2024

@dmikusa I have gathered the following shell-script that show cases the use-case where the buildpack is not working as expected without the proposed changes:

#!/usr/bin/env bash
gh repo clone jreleaser/helloworld-java-jar
cd helloworld-java-jar
./mvnw verify
pack build helloworld-java:latest \
  --path target/ \
  --builder paketobuildpacks/builder:base \
  --buildpack gcr.io/paketo-buildpacks/sap-machine \
  --buildpack gcr.io/paketo-buildpacks/syft \
  --buildpack gcr.io/paketo-buildpacks/executable-jar &&
docker run --rm helloworld-java:latest
cd -

@dmikusa
Copy link
Contributor

dmikusa commented Apr 25, 2024

Sorry for the delay. I got a chance to take a look at this and what you're doing with the target isn't really something that's supported presently.

pack build helloworld-java:latest \
  --path target/ 

You're pointing it to a whole folder of stuff and expecting the buildpack to pick through all that. The Java buildpacks expect one of two things: a.) an exploded JAR file that you've compiled in advance b.) Java source code that it can build.

If you take the demo app here and give it either of those things, then it produces a functional app image.

I think what you're proposing here is basically a fix for #132, and if I'm remembering this one correctly it came out of the work with Quarkus. You're hitting it in a slightly different use case, but it amounts to the same thing. You want to be able to set a path with pack build to a folder of one or more JAR files (in your case it's one, in the #132, it's multiple).

How it's implemented really isn't that different, with the exception that your PR doesn't handle the case where other JARs (or resources) in the folder need to be on the classpath. That works in your case, cause you have only the one FAT jar, but I believe Quarkus' "fast-jar" format has the executable JAR plus some other JARs that need to be on the classpath. In that case, this PR wouldn't work.

Would you be interested in submitting code changes to support that use case as well? (i.e. both #132 and #264)

There is a lot of testing and verification that's required for a change like this because it impacts the build plan and there are a lot of different scenarios to cover. It would be helpful if we could do it all at once.

@dmikusa dmikusa added type:enhancement A general enhancement semver:minor A change requiring a minor version bump labels Apr 25, 2024
@sap-ali
Copy link
Author

sap-ali commented Apr 30, 2024

@dmikusa Thanks for getting back to me. I would be happy to help with the other case, but since I don't have a good overview on all the possible cases that you mentioned, maybe we could somehow work on it together?

For example, you could write down all the tests that you think are necessary, and then I could see how I can implement the required functionality, or alternatively we could also schedule a 1h pairing session. What do you think?

@pbusko
Copy link

pbusko commented May 13, 2024

You're pointing it to a whole folder of stuff and expecting the buildpack to pick through all that. The Java buildpacks expect one of two things: a.) an exploded JAR file that you've compiled in advance b.) Java source code that it can build.

I think what you're proposing here is basically a fix for #132, and if I'm remembering this one correctly it came out of the work with Quarkus. You're hitting it in a slightly different use case, but it amounts to the same thing. You want to be able to set a path with pack build to a folder of one or more JAR files (in your case it's one, in the #132, it's multiple).

@dmikusa At the moment there's basically no way to build an image using executable jar and not exploding it. The use-case should be supported, given the following in the readme:

This buildpack will participate if all the following conditions are met:

  • <APPLICATION_ROOT>/**/*.jar exists and that JAR has a /META-INF/MANIFEST.MF file which contains a Main-Class entry

and

$BP_EXECUTABLE_JAR_LOCATION | An optional glob to specify the JAR used as an entrypoint. Defaults to "", which causes the buildpack to do a breadth-first search for the first executable JAR it finds.

if execJar.ExplodedJAR {
arguments = append(arguments, execJar.MainClass)
} else {
arguments = append(arguments, "-jar", execJar.Path)
}

The following is not working:

$ tree .
.
└── build
    └── runner.jar

2 directories, 1 file

$ pack build --verbose --trust-builder --builder paketobuildpacks/builder-jammy-base -b gcr.io/paketo-buildpacks/bellsoft-liberica -b gcr.io/paketo-buildpacks/syft -b gcr.io/paketo-buildpacks/executable-jar -e 'BP_EXECUTABLE_JAR_LOCATION=build/runner.jar' my-executable-jar

This scenario fails because the executable-jar buildpack completely ignores the BP_EXECUTABLE_JAR_LOCATION during detection and only reads it during build, which I would consider a bug in the current implementation.

@dmikusa
Copy link
Contributor

dmikusa commented May 13, 2024

@pbusko Yes, what you're referencing is exactly why #132 is needed. Those settings only apply when building from source code because detect hasn't been updated to support multiple JARs.

To be clear, I'd 100% like to see #132 implemented (I think it addresses #264 as well). This PR needs a little more though, see my previous notes.

@pbusko
Copy link

pbusko commented May 14, 2024

How it's implemented really isn't that different, with the exception that your PR doesn't handle the case where other JARs (or resources) in the folder need to be on the classpath. That works in your case, cause you have only the one FAT jar, but I believe Quarkus' "fast-jar" format has the executable JAR plus some other JARs that need to be on the classpath. In that case, this PR wouldn't work.

Do you have an example at hand? I just tried with Quarkus fatjar and the scenario described in #265 (comment) and everything works as expected.

Also the #132 states:

This enhancement request is to have detect also look at the present workspace and see if either a.) the workspace itself has a manifest or if b.) it can identify a single executable JAR file in the workspace.

Which is exactly what this PR does in my opinion.

@c0d1ngm0nk3y c0d1ngm0nk3y self-requested a review May 14, 2024 11:18
@c0d1ngm0nk3y
Copy link
Contributor

Which is exactly what this PR does in my opinion.

I agree that this pr looks good as it is. Imho it is rather a bugfix than a enhancement. The build logic handled the case well (by adding -jar), but it could never be detected.

Also this supports the most simplest use case - only a single executable jar present.

Copy link
Contributor

@c0d1ngm0nk3y c0d1ngm0nk3y left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some minor remarks - probably a matter of taste. But lgtm and fixes the bug.

@@ -65,9 +65,15 @@ func (d Detect) Detect(context libcnb.DetectContext) (libcnb.DetectResult, error
return libcnb.DetectResult{}, fmt.Errorf("unable to read manifest in %s\n%w", context.Application.Path, err)
}

jarGlob, _ := cr.Resolve("BP_EXECUTABLE_JAR_LOCATION")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we move those 2 lines into the else? Searching for a jar is only relevant if no "Main-Class" is found yet.

So something like

if m.Get("Main-Class") {
//case 1
} else {
  //search jar
 if found {
  //case 2
  }
}

Copy link

@pbusko pbusko May 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same checks are already present in

func LoadExecutableJAR(appPath string, executableJarGlob string) (ExecutableJAR, error) {

The detection logic can be minimised down to:

jarGlob, _ := cr.Resolve("BP_EXECUTABLE_JAR_LOCATION")
execJar, err := LoadExecutableJAR(context.Application.Path, jarGlob)
if err != nil {
	return libcnb.DetectResult{}, fmt.Errorf("unable to load executable JAR\n%w", err)
}

if !reflect.DeepEqual(execJar, ExecutableJAR{}) {
	d.Logger.Info("PASSED: 'Main-Class' manifest attribute found")
	result.Plans[0].Provides = append(result.Plans[0].Provides, libcnb.BuildPlanProvide{Name: PlanEntryJVMApplicationPackage})
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, updated :)

Copy link

@pbusko pbusko May 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sap-ali the idea was to replace everything with the suggested snippet, starting from the line 64:

m, err := libjvm.NewManifest(context.Application.Path)
if err != nil {
return libcnb.DetectResult{}, fmt.Errorf("unable to read manifest in %s\n%w", context.Application.Path, err)
}
if _, ok := m.Get("Main-Class"); ok {
d.Logger.Info("PASSED: 'Main-Class' manifest attribute found")
result.Plans[0].Provides = append(result.Plans[0].Provides, libcnb.BuildPlanProvide{Name: PlanEntryJVMApplicationPackage})
} else {
jarGlob, _ := cr.Resolve("BP_EXECUTABLE_JAR_LOCATION")
execJar, err := LoadExecutableJAR(context.Application.Path, jarGlob)
if err != nil {
return libcnb.DetectResult{}, fmt.Errorf("unable to load executable JAR\n%w", err)
}
if !reflect.DeepEqual(execJar, ExecutableJAR{}) {
d.Logger.Info("PASSED: 'Main-Class' manifest attribute found")
result.Plans[0].Provides = append(result.Plans[0].Provides, libcnb.BuildPlanProvide{Name: PlanEntryJVMApplicationPackage})
}
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pbusko Got you, thanks; updated.

executable/detect.go Outdated Show resolved Hide resolved
Copy link
Contributor

@c0d1ngm0nk3y c0d1ngm0nk3y left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sap-ali Could you have a look at this comment - should be a minor change.

@c0d1ngm0nk3y
Copy link
Contributor

@paketo-buildpacks/java-maintainers This fixes the use case that gives the buildpack its name (#264). This does not yet fix #132 , but imho it is a valuable improvement over the current state. Or do you see something missing?

@loewenstein
Copy link

@paketo-buildpacks/java-maintainers This fixes the use case that gives the buildpack its name (#264). This does not yet fix #132 , but imho it is a valuable improvement over the current state. Or do you see something missing?

@paketo-buildpacks/java-maintainers Would you mind to have a look?

@loewenstein
Copy link

@anthonydahanne & @pivotal-david-osullivan what's your take on this? I'd be much in favor of getting this merged as is. It's fixing a bug and that should not be coupled to feature development like #132 imho.

@anthonydahanne
Copy link
Member

what's your take on this? I'd be much in favor of getting this merged as is. It's fixing a bug and that should not be coupled to feature development like #132 imho.

Hello!
merging this contribution would require to run a decent number of tests; such as all the Java samples; and make sure the detection and plan are working properly for all use cases.
I'd very much prefer to perform this effort after #132 is also fixed / included in the fix so that we squash 2 related issues during 1 big regression testing session

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
semver:minor A change requiring a minor version bump type:enhancement A general enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Packing a single executable fat-Jar does not work
6 participants