paketo-buildpacks / native-image

A Cloud Native Buildpack that creates native images from Java applications
Apache License 2.0
46 stars 9 forks source link

Always builds a native-image event when explicitly disabled #289

Open cmdjulian opened 10 months ago

cmdjulian commented 10 months ago

I have a Spring Boot App with Gradle and Kotlin including id("org.graalvm.buildtools.native") version "0.9.26" and a META-INF/native-image folder in Resources. When running bootBuildImage with paketo base builder, it builds a native image. This is fine for prod uses. For some debugging I want to build a java based image, not a native image. When I now set BP_NATIVE_IMAGE=false, still a native image is build. I did try to exclude the plugin and also to exclude the META-INF folder, but regardless of what I try, the builder always builds a native image with liberica nik.

Expected Behavior

When setting BP_NATIVE_IMAGE=false I would expect to not build a native-image but rather a normal jvm based image.

Current Behavior

The builder builds a native image, regardless of which variables I set.

Possible Solution

-

Steps to Reproduce

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    val kotlinVersion = "1.9.10"
    kotlin("jvm") version kotlinVersion
    kotlin("plugin.spring") version kotlinVersion
    id("org.springframework.boot") version "3.1.3"
    id("io.spring.dependency-management") version "1.1.3"
    id("org.graalvm.buildtools.native") version "0.9.26"

    application
    id("org.flywaydb.flyway") version "9.22.0"
}

group = "com.example"

kotlin {
    jvmToolchain {
        languageVersion.set(JavaLanguageVersion.of(17))
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation(platform("org.springframework.cloud:spring-cloud-dependencies:2022.0.4"))
    implementation("org.springframework.boot:spring-boot-starter-actuator")
    implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
    implementation("org.springframework.boot:spring-boot-starter-validation")
    implementation("org.springframework.boot:spring-boot-starter-webflux")
    implementation("org.springframework.cloud:spring-cloud-starter-gateway")
    implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity6")
    runtimeOnly("io.r2dbc:r2dbc-h2")
    runtimeOnly("com.h2database:h2")
    runtimeOnly(kotlin("reflect"))

    // Kotlin
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactive")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")

    // Flyway
    implementation("org.flywaydb:flyway-core")
}

tasks {
    bootBuildImage {
        builder.set("paketobuildpacks/builder:base")
        environment.set(
            mapOf(
                "BP_JVM_VERSION" to "17",
                "BP_NATIVE_IMAGE" to "false",
                "BPE_SPRING_PROFILES_ACTIVE" to "prod",
                "BP_SPRING_CLOUD_BINDINGS_DISABLED" to "true",
                "BPE_APPEND_JAVA_TOOL_OPTIONS" to "-XX:+ExtensiveErrorReports",
                "BPE_DELIM_JAVA_TOOL_OPTIONS" to " ",
            ),
        )
        imageName.set("registry.gitlab.com/etalytics/infrastructure/eta-central")
        tags.set(listOf("${project.version}"))
    }
}

tasks.withType<KotlinCompile>().configureEach {
    kotlinOptions {
        javaParameters = true
        freeCompilerArgs = listOf("-Xjsr305=strict", "-Xemit-jvm-type-annotations", "-Xjvm-default=all", "-Xcontext-receivers")
    }
}

tasks.test {
    useJUnitPlatform()
}

graalvmNative {
    agent {
        defaultMode.set("standard")
    }
    toolchainDetection.set(false)
    binaries {
        all {
            resources.autodetect()
            buildArgs("--enable-monitoring=heapdump", "-march=native", "--initialize-at-build-time=org.slf4j.LoggerFactory,ch.qos.logback,org.apache.logging")
        }
        named("main") {
            when {
                project.hasProperty("static") -> buildArgs("--static", "--libc=musl")
                else -> buildArgs("-H:+StaticExecutableWithDynamicLibC")
            }
        }
    }
    metadataRepository {
        enabled.set(true)
    }
}

Motivations

dmikusa commented 10 months ago

You are correct that the behavior here is a bit odd. The buildpack doesn't actually look at the value, just if it's present or not.

If you simply remove it instead of setting it to false, you'll get the desired behavior.

We can look at changing this, but it would be a behavior/breaking change, so we'd be limited in terms of when we could introduce the change. I'll leave this as a bug request, but it could be a while before we can change this.

cmdjulian commented 10 months ago

The reason why I did set it in the first place, was that without setting it, the builder always builds a native image. So when using this one:

mapOf(
    "BP_JVM_VERSION" to "17",
    "BPE_SPRING_PROFILES_ACTIVE" to "prod",
    "BP_SPRING_CLOUD_BINDINGS_DISABLED" to "true",
    "BPE_APPEND_JAVA_TOOL_OPTIONS" to "-XX:+ExtensiveErrorReports",
    "BPE_DELIM_JAVA_TOOL_OPTIONS" to " ",
)

I still see the builder build a native image with liberika NIK

dmikusa commented 10 months ago

I'm not seeing that. The builder should include both the paketo-buildpacks/java and paketo-buildpacks/java-native-image so it should be capable of building a standard Java app as well as a Java native image app.

Also, paketobuildpacks/builder:base is the older Bionic base image set. It shouldn't be used anymore as Ubuntu Bionic is no longer supported by Canonical. The current builder is paketobuildpacks/builder-jammy-tiny. That's based on Jammy.

  1. Check that your builder has both of those buildpacks. Run pack builder inspect <builder>.
  2. Use the Jammy builder.
  3. If that doesn't help, include your build log so I can see what is happening in the build.

Thanks

cmdjulian commented 8 months ago

Sorry for the late reply. I now found the time to prepare a demo app. As you can see there, the only thing I did is to add org.graalvm.buildtools.native gradle plugin to my spring starter project. Without setting anything, it still builds a native image up on running bootBuildImage task, even when I explicitly set the flag to false (you did elaborate on that, I just wanted to mention it again).
I think this happens as the plugin creates a resource folder with META-INF content for native image resources.
I can elaborate on my use case here. When using the native image plugin, Spring automatically processes the context by writing out all its proxies into more efficient builder classes. These can than be used on a regular run to speed up the startup phase, even without using native image, resulting in a faster startup phase and overall a slightly smaller memory food print.
I think it is a very valid use case to include native image plugin to trigger the aot context processing without wanting to use native image.

demo.zip

cmdjulian commented 8 months ago

Okay it seems like it is dependent up on the MANIFEST.MF attribute Spring-Boot-Native-Processed = true not to any META-INF stuff. When dropping this from the jar, no native image is build, but then the app does not know it has to start as aot processed as well

dmikusa commented 8 months ago

Oh, I see. Yes, you're right. There were some changes not too far back to auto-detect when there's a Spring Boot app that is capable of being built with native image. I think the thought was that if you were doing this then you'd likely want to have a native image app image, so we defaulted to it.

A couple of thoughts:

  1. I believe the intent was that you could opt-out of this by setting BP_NATIVE_IMAGE=false so it sounds like this is not working as it should.
  2. I don't believe that we documented this new criteria to auto-detect Spring Boot native. As I look at the README, it doesn't mention this. We should get that updated too.

That said, I didn't make these changes so I'm going to defer to @anthonydahanne who introduced them. He would know best the intended behavior. Hopefully he can chime in soon on this issue.