├── .github ├── pipeline-version ├── CODEOWNERS ├── dependabot.yml ├── workflows │ ├── pb-synchronize-labels.yml │ ├── pb-minimal-labels.yml │ ├── pb-update-draft-release.yml │ ├── pb-update-go.yml │ ├── pb-update-pipeline.yml │ ├── pb-tests.yml │ └── pb-create-package.yml ├── pipeline-descriptor.yml ├── release-drafter.yml └── labels.yml ├── native ├── testdata │ ├── e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 │ │ └── stub-spring-graalvm-native.jar │ ├── test-fixture.jar │ └── e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.toml ├── init_test.go ├── slices │ └── slices.go ├── detect.go ├── build.go ├── arguments.go ├── native_image.go ├── build_test.go ├── arguments_test.go ├── native_image_test.go └── detect_test.go ├── NOTICE ├── scripts └── build.sh ├── .gitignore ├── cmd └── main │ └── main.go ├── go.mod ├── README.md ├── go.sum └── LICENSE /.github/pipeline-version: -------------------------------------------------------------------------------- 1 | 1.44.0 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @paketo-buildpacks/java-maintainers -------------------------------------------------------------------------------- /native/testdata/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855/stub-spring-graalvm-native.jar: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /native/testdata/test-fixture.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paketo-buildpacks/native-image/HEAD/native/testdata/test-fixture.jar -------------------------------------------------------------------------------- /native/testdata/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.toml: -------------------------------------------------------------------------------- 1 | uri = "https://localhost/stub-spring-graalvm-native.jar" 2 | sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: daily 7 | ignore: 8 | - dependency-name: github.com/onsi/gomega 9 | labels: 10 | - semver:patch 11 | - type:dependency-upgrade 12 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | native-image 2 | 3 | Copyright (c) 2020-Present CloudFoundry.org Foundation, Inc. All Rights Reserved. 4 | 5 | This project is licensed to you under the Apache License, Version 2.0 (the "License"). 6 | You may not use this project except in compliance with the License. 7 | 8 | This project may include a number of subcomponents with separate copyright notices 9 | and license terms. Your use of these subcomponents is subject to the terms and 10 | conditions of the subcomponent's license, as noted in the LICENSE file. 11 | -------------------------------------------------------------------------------- /.github/workflows/pb-synchronize-labels.yml: -------------------------------------------------------------------------------- 1 | name: Synchronize Labels 2 | "on": 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - .github/labels.yml 8 | jobs: 9 | synchronize: 10 | name: Synchronize Labels 11 | runs-on: 12 | - ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: micnncim/action-label-syncer@v1 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/pipeline-descriptor.yml: -------------------------------------------------------------------------------- 1 | github: 2 | username: ${{ secrets.JAVA_GITHUB_USERNAME }} 3 | token: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }} 4 | 5 | codeowners: 6 | - path: "*" 7 | owner: "@paketo-buildpacks/java-maintainers" 8 | 9 | package: 10 | repositories: ["docker.io/paketobuildpacks/native-image"] 11 | register: true 12 | registry_token: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }} 13 | 14 | docker_credentials: 15 | - registry: docker.io 16 | username: ${{ secrets.PAKETO_BUILDPACKS_DOCKERHUB_USERNAME }} 17 | password: ${{ secrets.PAKETO_BUILDPACKS_DOCKERHUB_PASSWORD }} 18 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | GOMOD=$(head -1 go.mod | awk '{print $2}') 5 | GOOS="linux" GOARCH="amd64" go build -ldflags='-s -w' -o linux/amd64/bin/main "$GOMOD/cmd/main" 6 | GOOS="linux" GOARCH="arm64" go build -ldflags='-s -w' -o linux/arm64/bin/main "$GOMOD/cmd/main" 7 | 8 | if [ "${STRIP:-false}" != "false" ]; then 9 | strip linux/amd64/bin/main linux/arm64/bin/main 10 | fi 11 | 12 | if [ "${COMPRESS:-none}" != "none" ]; then 13 | $COMPRESS linux/amd64/bin/main linux/arm64/bin/main 14 | fi 15 | 16 | ln -fs main linux/amd64/bin/build 17 | ln -fs main linux/arm64/bin/build 18 | ln -fs main linux/amd64/bin/detect 19 | ln -fs main linux/arm64/bin/detect -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2020 the original author or authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | bin/ 16 | linux/ 17 | dependencies/ 18 | package/ 19 | scratch/ 20 | 21 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | template: $CHANGES 2 | name-template: $RESOLVED_VERSION 3 | tag-template: v$RESOLVED_VERSION 4 | categories: 5 | - title: ⭐️ Enhancements 6 | labels: 7 | - type:enhancement 8 | - title: "\U0001F41E Bug Fixes" 9 | labels: 10 | - type:bug 11 | - title: "\U0001F4D4 Documentation" 12 | labels: 13 | - type:documentation 14 | - title: ⛏ Dependency Upgrades 15 | labels: 16 | - type:dependency-upgrade 17 | - title: "\U0001F6A7 Tasks" 18 | labels: 19 | - type:task 20 | exclude-labels: 21 | - type:question 22 | version-resolver: 23 | major: 24 | labels: 25 | - semver:major 26 | minor: 27 | labels: 28 | - semver:minor 29 | patch: 30 | labels: 31 | - semver:patch 32 | default: patch 33 | -------------------------------------------------------------------------------- /.github/workflows/pb-minimal-labels.yml: -------------------------------------------------------------------------------- 1 | name: Minimal Labels 2 | "on": 3 | pull_request: 4 | types: 5 | - synchronize 6 | - reopened 7 | - labeled 8 | - unlabeled 9 | jobs: 10 | semver: 11 | name: Minimal Semver Labels 12 | runs-on: 13 | - ubuntu-latest 14 | steps: 15 | - uses: mheap/github-action-required-labels@v5 16 | with: 17 | count: 1 18 | labels: semver:major, semver:minor, semver:patch 19 | mode: exactly 20 | type: 21 | name: Minimal Type Labels 22 | runs-on: 23 | - ubuntu-latest 24 | steps: 25 | - uses: mheap/github-action-required-labels@v5 26 | with: 27 | count: 1 28 | labels: type:bug, type:dependency-upgrade, type:documentation, type:enhancement, type:question, type:task 29 | mode: exactly 30 | -------------------------------------------------------------------------------- /.github/workflows/pb-update-draft-release.yml: -------------------------------------------------------------------------------- 1 | name: Update Draft Release 2 | "on": 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | update: 8 | name: Update Draft Release 9 | runs-on: 10 | - ubuntu-latest 11 | steps: 12 | - id: release-drafter 13 | uses: release-drafter/release-drafter@v5 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }} 16 | - uses: actions/checkout@v4 17 | - name: Update draft release with buildpack information 18 | uses: docker://ghcr.io/paketo-buildpacks/actions/draft-release:main 19 | with: 20 | github_token: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }} 21 | release_body: ${{ steps.release-drafter.outputs.body }} 22 | release_id: ${{ steps.release-drafter.outputs.id }} 23 | release_name: ${{ steps.release-drafter.outputs.name }} 24 | release_tag_name: ${{ steps.release-drafter.outputs.tag_name }} 25 | -------------------------------------------------------------------------------- /cmd/main/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "os" 21 | 22 | "github.com/paketo-buildpacks/libpak" 23 | "github.com/paketo-buildpacks/libpak/bard" 24 | 25 | "github.com/paketo-buildpacks/native-image/v5/native" 26 | ) 27 | 28 | func main() { 29 | logger := bard.NewLogger(os.Stdout) 30 | libpak.Main( 31 | native.Detect{Logger: logger}, 32 | native.Build{Logger: logger}, 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /native/init_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package native_test 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/sclevine/spec" 23 | "github.com/sclevine/spec/report" 24 | ) 25 | 26 | func TestUnit(t *testing.T) { 27 | suite := spec.New("native", spec.Report(report.Terminal{})) 28 | suite("Build", testBuild) 29 | suite("Detect", testDetect) 30 | suite("Arguments", testArguments) 31 | suite("NativeImage", testNativeImage) 32 | suite.Run(t) 33 | } 34 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | - name: semver:major 2 | description: A change requiring a major version bump 3 | color: f9d0c4 4 | - name: semver:minor 5 | description: A change requiring a minor version bump 6 | color: f9d0c4 7 | - name: semver:patch 8 | description: A change requiring a patch version bump 9 | color: f9d0c4 10 | - name: type:bug 11 | description: A general bug 12 | color: e3d9fc 13 | - name: type:dependency-upgrade 14 | description: A dependency upgrade 15 | color: e3d9fc 16 | - name: type:documentation 17 | description: A documentation update 18 | color: e3d9fc 19 | - name: type:enhancement 20 | description: A general enhancement 21 | color: e3d9fc 22 | - name: type:question 23 | description: A user question 24 | color: e3d9fc 25 | - name: type:task 26 | description: A general task 27 | color: e3d9fc 28 | - name: type:informational 29 | description: Provides information or notice to the community 30 | color: e3d9fc 31 | - name: type:poll 32 | description: Request for feedback from the community 33 | color: e3d9fc 34 | - name: note:ideal-for-contribution 35 | description: An issue that a contributor can help us with 36 | color: 54f7a8 37 | - name: note:on-hold 38 | description: We can't start working on this issue yet 39 | color: 54f7a8 40 | - name: note:good-first-issue 41 | description: A good first issue to get started with 42 | color: 54f7a8 43 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/paketo-buildpacks/native-image/v5 2 | 3 | go 1.25 4 | 5 | require ( 6 | github.com/buildpacks/libcnb v1.30.4 7 | github.com/heroku/color v0.0.6 8 | github.com/magiconair/properties v1.8.10 9 | github.com/mattn/go-colorable v0.1.14 // indirect 10 | github.com/mattn/go-shellwords v1.0.12 11 | github.com/onsi/gomega v1.38.3 12 | github.com/paketo-buildpacks/libjvm v1.46.0 13 | github.com/paketo-buildpacks/libpak v1.73.0 14 | github.com/sclevine/spec v1.4.0 15 | github.com/stretchr/testify v1.11.1 16 | ) 17 | 18 | require ( 19 | github.com/BurntSushi/toml v1.5.0 // indirect 20 | github.com/Masterminds/semver/v3 v3.4.0 // indirect 21 | github.com/creack/pty v1.1.24 // indirect 22 | github.com/davecgh/go-spew v1.1.1 // indirect 23 | github.com/google/go-cmp v0.7.0 // indirect 24 | github.com/h2non/filetype v1.1.3 // indirect 25 | github.com/imdario/mergo v0.3.16 // indirect 26 | github.com/mattn/go-isatty v0.0.20 // indirect 27 | github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect 28 | github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0 // indirect 29 | github.com/pmezard/go-difflib v1.0.0 // indirect 30 | github.com/stretchr/objx v0.5.3 // indirect 31 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect 32 | go.yaml.in/yaml/v3 v3.0.4 // indirect 33 | golang.org/x/crypto v0.46.0 // indirect 34 | golang.org/x/net v0.48.0 // indirect 35 | golang.org/x/sys v0.39.0 // indirect 36 | golang.org/x/text v0.32.0 // indirect 37 | gopkg.in/yaml.v3 v3.0.1 // indirect 38 | software.sslmate.com/src/go-pkcs12 v0.6.0 // indirect 39 | ) 40 | -------------------------------------------------------------------------------- /native/slices/slices.go: -------------------------------------------------------------------------------- 1 | package slices 2 | 3 | // Those 2 functions were copied from https://cs.opensource.google/go/x/exp/+/master:slices/slices.go , 4 | // an experimental collection of utility methods that could be promoted to stdlib in the future 5 | 6 | //Copyright (c) 2009 The Go Authors. All rights reserved. 7 | // 8 | //Redistribution and use in source and binary forms, with or without 9 | //modification, are permitted provided that the following conditions are 10 | //met: 11 | // 12 | // * Redistributions of source code must retain the above copyright 13 | //notice, this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above 15 | //copyright notice, this list of conditions and the following disclaimer 16 | //in the documentation and/or other materials provided with the 17 | //distribution. 18 | // * Neither the name of Google Inc. nor the names of its 19 | //contributors may be used to endorse or promote products derived from 20 | //this software without specific prior written permission. 21 | // 22 | //THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | //"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | //LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25 | //A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 26 | //OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 27 | //SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 28 | //LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 | //DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 30 | //THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 31 | //(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | //OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | 34 | // Index returns the index of the first occurrence of v in s, 35 | // or -1 if not present. 36 | func Index[E comparable](s []E, v E) int { 37 | for i := range s { 38 | if v == s[i] { 39 | return i 40 | } 41 | } 42 | return -1 43 | } 44 | 45 | // Contains reports whether v is present in s. 46 | func Contains[E comparable](s []E, v E) bool { 47 | return Index(s, v) >= 0 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/pb-update-go.yml: -------------------------------------------------------------------------------- 1 | name: Update Go 2 | "on": 3 | schedule: 4 | - cron: 2 2 * * 1 5 | workflow_dispatch: {} 6 | jobs: 7 | update: 8 | name: Update Go 9 | runs-on: 10 | - ubuntu-latest 11 | steps: 12 | - uses: actions/setup-go@v5 13 | with: 14 | go-version: "1.25" 15 | - uses: actions/checkout@v4 16 | - name: Update Go Version & Modules 17 | id: update-go 18 | run: | 19 | #!/usr/bin/env bash 20 | 21 | set -euo pipefail 22 | 23 | if [ -z "${GO_VERSION:-}" ]; then 24 | echo "No go version set" 25 | exit 1 26 | fi 27 | 28 | OLD_GO_VERSION=$(grep -P '^go \d\.\d+' go.mod | cut -d ' ' -f 2 | cut -d '.' -f 1-2) 29 | 30 | go mod edit -go="$GO_VERSION" 31 | go mod tidy 32 | go get -u -t ./... 33 | go mod tidy 34 | 35 | git add go.mod go.sum 36 | git checkout -- . 37 | 38 | if [ "$OLD_GO_VERSION" == "$GO_VERSION" ]; then 39 | COMMIT_TITLE="Bump Go Modules" 40 | COMMIT_BODY="Bumps Go modules used by the project. See the commit for details on what modules were updated." 41 | COMMIT_SEMVER="semver:patch" 42 | else 43 | COMMIT_TITLE="Bump Go from ${OLD_GO_VERSION} to ${GO_VERSION}" 44 | COMMIT_BODY="Bumps Go from ${OLD_GO_VERSION} to ${GO_VERSION} and update Go modules used by the project. See the commit for details on what modules were updated." 45 | COMMIT_SEMVER="semver:minor" 46 | fi 47 | 48 | echo "commit-title=${COMMIT_TITLE}" >> "$GITHUB_OUTPUT" 49 | echo "commit-body=${COMMIT_BODY}" >> "$GITHUB_OUTPUT" 50 | echo "commit-semver=${COMMIT_SEMVER}" >> "$GITHUB_OUTPUT" 51 | env: 52 | GO_VERSION: "1.25" 53 | - uses: peter-evans/create-pull-request@v6 54 | with: 55 | author: ${{ secrets.JAVA_GITHUB_USERNAME }} <${{ secrets.JAVA_GITHUB_USERNAME }}@users.noreply.github.com> 56 | body: |- 57 | ${{ steps.update-go.outputs.commit-body }} 58 | 59 |
60 | Release Notes 61 | ${{ steps.pipeline.outputs.release-notes }} 62 |
63 | branch: update/go 64 | commit-message: |- 65 | ${{ steps.update-go.outputs.commit-title }} 66 | 67 | ${{ steps.update-go.outputs.commit-body }} 68 | delete-branch: true 69 | labels: ${{ steps.update-go.outputs.commit-semver }}, type:task 70 | signoff: true 71 | title: ${{ steps.update-go.outputs.commit-title }} 72 | token: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }} 73 | -------------------------------------------------------------------------------- /.github/workflows/pb-update-pipeline.yml: -------------------------------------------------------------------------------- 1 | name: Update Pipeline 2 | "on": 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - .github/pipeline-descriptor.yml 8 | schedule: 9 | - cron: 0 5 * * 1-5 10 | workflow_dispatch: {} 11 | jobs: 12 | update: 13 | name: Update Pipeline 14 | runs-on: 15 | - ubuntu-latest 16 | steps: 17 | - uses: actions/setup-go@v5 18 | with: 19 | go-version: "1.25" 20 | - name: Install octo 21 | run: | 22 | #!/usr/bin/env bash 23 | 24 | set -euo pipefail 25 | 26 | go install -ldflags="-s -w" github.com/paketo-buildpacks/pipeline-builder/cmd/octo@latest 27 | - uses: actions/checkout@v4 28 | - name: Update Pipeline 29 | id: pipeline 30 | run: | 31 | #!/usr/bin/env bash 32 | 33 | set -euo pipefail 34 | 35 | if [[ -f .github/pipeline-version ]]; then 36 | OLD_VERSION=$(cat .github/pipeline-version) 37 | else 38 | OLD_VERSION="0.0.0" 39 | fi 40 | 41 | rm .github/workflows/pb-*.yml || true 42 | octo --descriptor "${DESCRIPTOR}" 43 | 44 | PAYLOAD=$(gh api /repos/paketo-buildpacks/pipeline-builder/releases/latest) 45 | 46 | NEW_VERSION=$(jq -n -r --argjson PAYLOAD "${PAYLOAD}" '$PAYLOAD.name') 47 | echo "${NEW_VERSION}" > .github/pipeline-version 48 | 49 | RELEASE_NOTES=$( 50 | gh api \ 51 | -F text="$(jq -n -r --argjson PAYLOAD "${PAYLOAD}" '$PAYLOAD.body')" \ 52 | -F mode="gfm" \ 53 | -F context="paketo-buildpacks/pipeline-builder" \ 54 | -X POST /markdown 55 | ) 56 | 57 | git add .github/ 58 | git add .gitignore 59 | 60 | if [ -f scripts/build.sh ]; then 61 | git add scripts/build.sh 62 | fi 63 | 64 | git checkout -- . 65 | 66 | echo "old-version=${OLD_VERSION}" >> "$GITHUB_OUTPUT" 67 | echo "new-version=${NEW_VERSION}" >> "$GITHUB_OUTPUT" 68 | 69 | DELIMITER=$(openssl rand -hex 16) # roughly the same entropy as uuid v4 used in https://github.com/actions/toolkit/blob/b36e70495fbee083eb20f600eafa9091d832577d/packages/core/src/file-command.ts#L28 70 | printf "release-notes<<%s\n%s\n%s\n" "${DELIMITER}" "${RELEASE_NOTES}" "${DELIMITER}" >> "${GITHUB_OUTPUT}" # see https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings 71 | env: 72 | DESCRIPTOR: .github/pipeline-descriptor.yml 73 | GITHUB_TOKEN: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }} 74 | - uses: peter-evans/create-pull-request@v6 75 | with: 76 | author: ${{ secrets.JAVA_GITHUB_USERNAME }} <${{ secrets.JAVA_GITHUB_USERNAME }}@users.noreply.github.com> 77 | body: |- 78 | Bumps pipeline from `${{ steps.pipeline.outputs.old-version }}` to `${{ steps.pipeline.outputs.new-version }}`. 79 | 80 |
81 | Release Notes 82 | ${{ steps.pipeline.outputs.release-notes }} 83 |
84 | branch: update/pipeline 85 | commit-message: |- 86 | Bump pipeline from ${{ steps.pipeline.outputs.old-version }} to ${{ steps.pipeline.outputs.new-version }} 87 | 88 | Bumps pipeline from ${{ steps.pipeline.outputs.old-version }} to ${{ steps.pipeline.outputs.new-version }}. 89 | delete-branch: true 90 | labels: semver:patch, type:task 91 | signoff: true 92 | title: Bump pipeline from ${{ steps.pipeline.outputs.old-version }} to ${{ steps.pipeline.outputs.new-version }} 93 | token: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }} 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Paketo Buildpack for Native Image 2 | 3 | ## Buildpack ID: `paketo-buildpacks/native-image` 4 | ## Registry URLs: `docker.io/paketobuildpacks/native-image` 5 | 6 | The Paketo Buildpack for Native Image is a Cloud Native Buildpack that uses the [GraalVM Native Image builder][native-image] (`native-image`) to compile a standalone executable from an executable JAR. 7 | 8 | Most users should not use this component buildpack directly and should instead use the [Paketo Java Native Image][bp/java-native-image], which provides the full set of buildpacks required to build a native image application. 9 | 10 | ## Behavior 11 | 12 | This buildpack will participate if one the following conditions are met: 13 | 14 | * `$BP_NATIVE_IMAGE` is set. 15 | * An upstream buildpack requests `native-image-application` in the build plan. 16 | * An upstream buildpack provides `native-processed` in the build plan. 17 | 18 | The buildpack will do the following: 19 | 20 | * Requests that the Native Image builder be installed by requiring `native-image-builder` in the build plan. 21 | * If `$BP_BINARY_COMPRESSION_METHOD` is set to `upx`, requests that UPX be installed by requiring `upx` in the buildplan. 22 | * Uses `native-image` to build a GraalVM native image and removes existing bytecode. Defaults to building the `/workspace` as an exploded JAR. If `$BP_NATIVE_IMAGE_BUILT_ARTIFACT` is set, it will build from the specified JAR file. 23 | * Uses `$BP_BINARY_COMPRESSION_METHOD` if set to `upx` or `gzexe` to compress the native image. 24 | 25 | ## Configuration 26 | 27 | | Environment Variable | Description | 28 | | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 29 | | `$BP_NATIVE_IMAGE` | Whether to build a native image from the application. Defaults to false. | 30 | | `$BP_NATIVE_IMAGE_BUILD_ARGUMENTS` | Arguments to pass to directly to the `native-image` command. These arguments must be valid and correctly formed or the `native-image` command will fail. | 31 | | `$BP_NATIVE_IMAGE_BUILD_ARGUMENTS_FILE` | A file containing arguments to pass to directly to the `native-image` command. The file must exist and the contents must be valid and correctly formed or the `native-image` command will fail. The file must follow the `@argument` file format as [specified by Java](https://docs.oracle.com/javase/8/docs/technotes/tools/unix/javac.html#BHCJEIBB). An argument file can be space-separated, EOL-separated, or a mix of both. We suggest sticking with one or the other, mixed separator support is best-effort only. | 32 | | `$BP_BINARY_COMPRESSION_METHOD` | Compression mechanism used to reduce binary size. Options: `none` (default), `upx` or `gzexe` | 33 | | `$BP_NATIVE_IMAGE_BUILT_ARTIFACT` | Configure the built application artifact explicitly. This is required if building a native image from a JAR file | 34 | 35 | ### Compression Caveats 36 | 37 | 1. Using `gzexe` if you intend to run your application on a Paketo Tiny image is not currently supported. The `gzexe` utility will compress your executable into what is a shell script, which executes and extracts the actual binary to a temp location. This process requires `/bin/sh` and that is not in the Tiny images. If you try using `gzexe` with a Tiny stack, it'll build OK but fail to run saying a file is missing. 38 | 39 | 2. Using `upx` will create a compressed executable that fails to run on M1 Macs. There is at the time of writing a bug in the emulation layer used by Docker on M1 Macs that is triggered when you try to run amd64 executable that has been compressed using `upx`. This is a known issue and will hopefully be patched in a future release. 40 | 41 | ## License 42 | 43 | This buildpack is released under version 2.0 of the [Apache License][a]. 44 | 45 | [a]: http://www.apache.org/licenses/LICENSE-2.0 46 | [native-image]: https://www.graalvm.org/reference-manual/native-image/ 47 | [bp/java-native-image]: https://github.com/paketo-buildpacks/java-native-image 48 | 49 | -------------------------------------------------------------------------------- /native/detect.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package native 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/buildpacks/libcnb" 23 | "github.com/paketo-buildpacks/libpak" 24 | "github.com/paketo-buildpacks/libpak/bard" 25 | "github.com/paketo-buildpacks/libpak/sherpa" 26 | ) 27 | 28 | const ( 29 | ConfigNativeImage = "BP_NATIVE_IMAGE" 30 | DeprecatedConfigNativeImage = "BP_BOOT_NATIVE_IMAGE" 31 | BinaryCompressionMethod = "BP_BINARY_COMPRESSION_METHOD" 32 | 33 | PlanEntryNativeImage = "native-image-application" 34 | PlanEntryNativeProcessed = "native-processed" 35 | PlanEntryNativeImageBuilder = "native-image-builder" 36 | PlanEntryJVMApplication = "jvm-application" 37 | PlanEntrySpringBoot = "spring-boot" 38 | PlanEntryUpx = "upx" 39 | ) 40 | 41 | type Detect struct { 42 | Logger bard.Logger 43 | } 44 | 45 | func (d Detect) Detect(context libcnb.DetectContext) (libcnb.DetectResult, error) { 46 | cr, err := libpak.NewConfigurationResolver(context.Buildpack, nil) 47 | if err != nil { 48 | return libcnb.DetectResult{}, fmt.Errorf("unable to create configuration resolver\n%w", err) 49 | } 50 | 51 | result := libcnb.DetectResult{ 52 | Pass: true, 53 | Plans: []libcnb.BuildPlan{ 54 | { 55 | Provides: []libcnb.BuildPlanProvide{ 56 | { 57 | Name: PlanEntryNativeImage, 58 | }, 59 | }, 60 | Requires: []libcnb.BuildPlanRequire{ 61 | { 62 | Name: PlanEntryNativeImageBuilder, 63 | }, 64 | { 65 | Name: PlanEntryJVMApplication, 66 | Metadata: map[string]interface{}{"native-image": true}, 67 | }, 68 | { 69 | Name: PlanEntrySpringBoot, 70 | Metadata: map[string]interface{}{"native-image": true}, 71 | }, 72 | }, 73 | }, 74 | { 75 | Provides: []libcnb.BuildPlanProvide{ 76 | { 77 | Name: PlanEntryNativeImage, 78 | }, 79 | }, 80 | Requires: []libcnb.BuildPlanRequire{ 81 | { 82 | Name: PlanEntryNativeImageBuilder, 83 | }, 84 | { 85 | Name: PlanEntryNativeProcessed, 86 | }, 87 | { 88 | Name: PlanEntryNativeImage, 89 | }, 90 | }, 91 | }, 92 | { 93 | Provides: []libcnb.BuildPlanProvide{ 94 | { 95 | Name: PlanEntryNativeImage, 96 | }, 97 | }, 98 | Requires: []libcnb.BuildPlanRequire{ 99 | { 100 | Name: PlanEntryNativeImageBuilder, 101 | }, 102 | { 103 | Name: PlanEntryJVMApplication, 104 | Metadata: map[string]interface{}{"native-image": true}, 105 | }, 106 | }, 107 | }, 108 | }, 109 | } 110 | 111 | if ok, err := d.nativeImageEnabled(cr); err != nil { 112 | d.Logger.Infof("SKIPPED: The BP_NATIVE_IMAGE environment variable was not set to true") 113 | return libcnb.DetectResult{}, err 114 | } else if ok { 115 | for i := range result.Plans { 116 | found := false 117 | for _, r := range result.Plans[i].Requires { 118 | if r.Name == PlanEntryNativeImage { 119 | found = true 120 | } 121 | } 122 | if !found { 123 | result.Plans[i].Requires = append(result.Plans[i].Requires, libcnb.BuildPlanRequire{ 124 | Name: PlanEntryNativeImage, 125 | }) 126 | } 127 | } 128 | } else { 129 | d.Logger.Infof("BP_NATIVE_IMAGE environment variable was not set to true, %s will not be required", PlanEntryNativeImage) 130 | } 131 | 132 | if d.upxCompressionEnabled(cr) { 133 | for i := range result.Plans { 134 | result.Plans[i].Requires = append(result.Plans[i].Requires, libcnb.BuildPlanRequire{ 135 | Name: PlanEntryUpx, 136 | }) 137 | } 138 | } 139 | 140 | // still participates if a downstream buildpack requires native-image-applications or upx 141 | return result, nil 142 | } 143 | 144 | func (d Detect) upxCompressionEnabled(cr libpak.ConfigurationResolver) bool { 145 | if val, ok := cr.Resolve(BinaryCompressionMethod); ok { 146 | return val == CompressorUpx 147 | } 148 | return false 149 | } 150 | 151 | func (d Detect) nativeImageEnabled(cr libpak.ConfigurationResolver) (bool, error) { 152 | if _, ok := cr.Resolve(ConfigNativeImage); ok { 153 | return sherpa.ResolveBoolErr(ConfigNativeImage) 154 | } 155 | return sherpa.ResolveBoolErr(DeprecatedConfigNativeImage) 156 | } 157 | -------------------------------------------------------------------------------- /native/build.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package native 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "os" 23 | "path/filepath" 24 | 25 | "github.com/paketo-buildpacks/libpak/sherpa" 26 | 27 | "github.com/paketo-buildpacks/libpak/effect" 28 | "github.com/paketo-buildpacks/libpak/sbom" 29 | 30 | "github.com/buildpacks/libcnb" 31 | "github.com/heroku/color" 32 | "github.com/magiconair/properties" 33 | "github.com/paketo-buildpacks/libjvm" 34 | "github.com/paketo-buildpacks/libpak" 35 | "github.com/paketo-buildpacks/libpak/bard" 36 | ) 37 | 38 | const ( 39 | ConfigNativeImageArgs = "BP_NATIVE_IMAGE_BUILD_ARGUMENTS" 40 | DeprecatedConfigNativeImageArgs = "BP_BOOT_NATIVE_IMAGE_BUILD_ARGUMENTS" 41 | CompressorUpx = "upx" 42 | CompressorGzexe = "gzexe" 43 | CompressorNone = "none" 44 | ) 45 | 46 | type Build struct { 47 | Logger bard.Logger 48 | SBOMScanner sbom.SBOMScanner 49 | } 50 | 51 | func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { 52 | cr, err := libpak.NewConfigurationResolver(context.Buildpack, nil) // so it doesn't print the configuration 53 | if err != nil { 54 | return libcnb.BuildResult{}, fmt.Errorf("unable to create configuration resolver\n%w", err) 55 | } 56 | 57 | // Check if BP_NATIVE_IMAGE or BP_BOOT_NATIVE_IMAGE are specifically set to false then we skip the build 58 | _, runNativeImageSet := cr.Resolve(ConfigNativeImage) 59 | _, runDeprecatedNativeImageSet := cr.Resolve(DeprecatedConfigNativeImage) 60 | if runNativeImageSet || runDeprecatedNativeImageSet { 61 | runNativeImageBuild := sherpa.ResolveBool(ConfigNativeImage) || sherpa.ResolveBool(DeprecatedConfigNativeImage) 62 | if !runNativeImageBuild { 63 | return libcnb.NewBuildResult(), nil 64 | } 65 | } 66 | 67 | b.Logger.Title(context.Buildpack) 68 | result := libcnb.NewBuildResult() 69 | 70 | manifest, err := libjvm.NewManifest(context.Application.Path) 71 | if err != nil { 72 | return libcnb.BuildResult{}, fmt.Errorf("unable to read manifest in %s\n%w", context.Application.Path, err) 73 | } 74 | 75 | cr, err = libpak.NewConfigurationResolver(context.Buildpack, &b.Logger) 76 | if err != nil { 77 | return libcnb.BuildResult{}, fmt.Errorf("unable to create configuration resolver\n%w", err) 78 | } 79 | 80 | if _, ok := cr.Resolve(DeprecatedConfigNativeImage); ok { 81 | warn(b.Logger, fmt.Sprintf("$%s has been deprecated. Please use $%s instead.", 82 | DeprecatedConfigNativeImage, 83 | ConfigNativeImage, 84 | )) 85 | } 86 | 87 | args, ok := cr.Resolve(ConfigNativeImageArgs) 88 | if !ok { 89 | if args, ok = cr.Resolve(DeprecatedConfigNativeImageArgs); ok { 90 | warn(b.Logger, fmt.Sprintf("$%s has been deprecated. Please use $%s instead.", 91 | DeprecatedConfigNativeImageArgs, 92 | ConfigNativeImageArgs, 93 | )) 94 | } 95 | } 96 | 97 | jarFilePattern, _ := cr.Resolve("BP_NATIVE_IMAGE_BUILT_ARTIFACT") 98 | argsFile, _ := cr.Resolve("BP_NATIVE_IMAGE_BUILD_ARGUMENTS_FILE") 99 | 100 | if argsFile != "" { 101 | argsFile, err = filepath.Abs(argsFile) 102 | if err != nil { 103 | return libcnb.BuildResult{}, fmt.Errorf("unable to create absolute path for native image argfile %s\n%w", argsFile, err) 104 | } 105 | 106 | if exists, err := sherpa.Exists(argsFile); err != nil { 107 | return libcnb.BuildResult{}, fmt.Errorf("unable to check for native-image arguments file at %s\n%w", argsFile, err) 108 | } else if !exists { 109 | argsFile = "" 110 | } 111 | } 112 | 113 | compressor, ok := cr.Resolve(BinaryCompressionMethod) 114 | if !ok { 115 | compressor = CompressorNone 116 | } else if ok { 117 | if compressor != CompressorUpx && compressor != CompressorGzexe && compressor != CompressorNone { 118 | warn(b.Logger, fmt.Sprintf("Requested compression method [%s] is unknown, no compression will be performed", compressor)) 119 | compressor = CompressorNone 120 | } 121 | } 122 | 123 | n, err := NewNativeImage(context.Application.Path, args, argsFile, compressor, jarFilePattern, manifest, context.StackID) 124 | if err != nil { 125 | return libcnb.BuildResult{}, fmt.Errorf("unable to create native image layer\n%w", err) 126 | } 127 | n.Logger = b.Logger 128 | result.Layers = append(result.Layers, n) 129 | 130 | startClass, err := findStartOrMainClass(manifest, context.Application.Path, jarFilePattern) 131 | if err != nil { 132 | return libcnb.BuildResult{}, fmt.Errorf("unable to find required manifest property\n%w", err) 133 | } 134 | 135 | command := fmt.Sprintf("%c%c%s", '.', os.PathSeparator, startClass) 136 | result.Processes = append(result.Processes, 137 | libcnb.Process{Type: "native-image", Command: command, Direct: true}, 138 | libcnb.Process{Type: "task", Command: command, Direct: true}, 139 | libcnb.Process{Type: "web", Command: command, Direct: true, Default: true}, 140 | ) 141 | 142 | if b.SBOMScanner == nil { 143 | b.SBOMScanner = sbom.NewSyftCLISBOMScanner(context.Layers, effect.CommandExecutor{}, b.Logger) 144 | } 145 | if err := b.SBOMScanner.ScanLaunch(context.Application.Path, libcnb.SyftJSON, libcnb.CycloneDXJSON); err != nil { 146 | return libcnb.BuildResult{}, fmt.Errorf("unable to create Build SBoM \n%w", err) 147 | } 148 | 149 | return result, nil 150 | } 151 | 152 | // todo: move warn method to the logger 153 | func warn(l bard.Logger, msg string) { 154 | l.Headerf( 155 | "\n%s %s\n\n", 156 | color.New(color.FgYellow, color.Bold).Sprintf("Warning:"), 157 | msg, 158 | ) 159 | } 160 | 161 | func findStartOrMainClass(manifest *properties.Properties, appPath, jarFilePattern string) (string, error) { 162 | _, startClass, err := ExplodedJarArguments{Manifest: manifest}.Configure(nil) 163 | if err != nil && !errors.Is(err, NoStartOrMainClass{}) { 164 | return "", fmt.Errorf("unable to find startClass\n%w", err) 165 | } 166 | 167 | if startClass != "" { 168 | return startClass, nil 169 | } 170 | 171 | _, startClass, err = JarArguments{JarFilePattern: jarFilePattern, ApplicationPath: appPath}.Configure(nil) 172 | if err != nil { 173 | return "", fmt.Errorf("unable to find startClass from JAR\n%w", err) 174 | } 175 | 176 | if startClass != "" { 177 | return startClass, nil 178 | } 179 | 180 | return "", fmt.Errorf("unable to find a suitable startClass") 181 | } 182 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= 2 | github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 3 | github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= 4 | github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 5 | github.com/buildpacks/libcnb v1.30.4 h1:Jp6cJxYsZQgqix+lpRdSpjHt5bv5yCJqgkw9zWmS6xU= 6 | github.com/buildpacks/libcnb v1.30.4/go.mod h1:vjEDAlK3/Rf67AcmBzphXoqIlbdFgBNUK5d8wjreJbY= 7 | github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= 8 | github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 12 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 13 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 14 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 15 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 16 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 17 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= 18 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 19 | github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= 20 | github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= 21 | github.com/heroku/color v0.0.6 h1:UTFFMrmMLFcL3OweqP1lAdp8i1y/9oHqkeHjQ/b/Ny0= 22 | github.com/heroku/color v0.0.6/go.mod h1:ZBvOcx7cTF2QKOv4LbmoBtNl5uB17qWxGuzZrsi1wLU= 23 | github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= 24 | github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= 25 | github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= 26 | github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= 27 | github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= 28 | github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 29 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 30 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 31 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 32 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 33 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 34 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 35 | github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= 36 | github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= 37 | github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= 38 | github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= 39 | github.com/onsi/ginkgo/v2 v2.25.3 h1:Ty8+Yi/ayDAGtk4XxmmfUy4GabvM+MegeB4cDLRi6nw= 40 | github.com/onsi/ginkgo/v2 v2.25.3/go.mod h1:43uiyQC4Ed2tkOzLsEYm7hnrb7UJTWHYNsuy3bG/snE= 41 | github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM= 42 | github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= 43 | github.com/paketo-buildpacks/libjvm v1.46.0 h1:+mEOsK30a1if0T3ZvSs6di/w5cp/j14uD3DyYUusavI= 44 | github.com/paketo-buildpacks/libjvm v1.46.0/go.mod h1:jNQuS8SQfbbHN9kenMpBhpxaE0uCa9ZkKLrN89IU0VY= 45 | github.com/paketo-buildpacks/libpak v1.73.0 h1:OgdkOn4VLIzRo0WcSx1iRmqeLrcMAZbIk7pOOJSyl5Q= 46 | github.com/paketo-buildpacks/libpak v1.73.0/go.mod h1:EY01BAEtNPT1kI+/OTGTAkitNzKiFzCTGAmxapBUPJ4= 47 | github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0 h1:2nosf3P75OZv2/ZO/9Px5ZgZ5gbKrzA3joN1QMfOGMQ= 48 | github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0/go.mod h1:lAVhWwbNaveeJmxrxuSTxMgKpF6DjnuVpn6T8WiBwYQ= 49 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 50 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 51 | github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8= 52 | github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM= 53 | github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= 54 | github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= 55 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 56 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 57 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= 58 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= 59 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= 60 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 61 | go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= 62 | go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 63 | golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= 64 | golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= 65 | golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= 66 | golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= 67 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 68 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 69 | golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= 70 | golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 71 | golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= 72 | golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 73 | golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= 74 | golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= 75 | google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= 76 | google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 77 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 78 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 79 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 80 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 81 | software.sslmate.com/src/go-pkcs12 v0.6.0 h1:f3sQittAeF+pao32Vb+mkli+ZyT+VwKaD014qFGq6oU= 82 | software.sslmate.com/src/go-pkcs12 v0.6.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= 83 | -------------------------------------------------------------------------------- /native/arguments.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package native 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "path/filepath" 23 | "sort" 24 | "strings" 25 | 26 | "github.com/magiconair/properties" 27 | "github.com/mattn/go-shellwords" 28 | "github.com/paketo-buildpacks/libpak" 29 | ) 30 | 31 | type Arguments interface { 32 | Configure(inputArgs []string) ([]string, string, error) 33 | } 34 | 35 | // BaselineArguments provides a set of arguments that are always set 36 | type BaselineArguments struct { 37 | StackID string 38 | } 39 | 40 | // Configure provides an initial set of arguments, it ignores any input arguments 41 | func (b BaselineArguments) Configure(_ []string) ([]string, string, error) { 42 | var newArguments []string 43 | 44 | if libpak.IsTinyStack(b.StackID) { 45 | newArguments = append(newArguments, "-H:+StaticExecutableWithDynamicLibC") 46 | } 47 | 48 | return newArguments, "", nil 49 | } 50 | 51 | // UserArguments augments the existing arguments with those provided by the end user 52 | type UserArguments struct { 53 | Arguments string 54 | } 55 | 56 | // Configure returns the inputArgs plus the additional arguments specified by the end user, preference given to user arguments 57 | func (u UserArguments) Configure(inputArgs []string) ([]string, string, error) { 58 | parsedArgs, err := shellwords.Parse(u.Arguments) 59 | if err != nil { 60 | return []string{}, "", fmt.Errorf("unable to parse arguments from %s\n%w", u.Arguments, err) 61 | } 62 | 63 | var outputArgs []string 64 | 65 | for _, inputArg := range inputArgs { 66 | if !containsArg(inputArg, parsedArgs) { 67 | outputArgs = append(outputArgs, inputArg) 68 | } 69 | } 70 | 71 | outputArgs = append(outputArgs, parsedArgs...) 72 | 73 | return outputArgs, "", nil 74 | } 75 | 76 | // UserFileArguments augments the existing arguments with those provided by the end user through a file 77 | type UserFileArguments struct { 78 | ArgumentsFile string 79 | } 80 | 81 | // Configure returns the inputArgs plus the additional arguments provided via argfile, setting via the '@argfile' format 82 | func (u UserFileArguments) Configure(inputArgs []string) ([]string, string, error) { 83 | 84 | rawArgs, err := os.ReadFile(u.ArgumentsFile) 85 | if err != nil { 86 | return []string{}, "", fmt.Errorf("read arguments from %s\n%w", u.ArgumentsFile, err) 87 | } 88 | 89 | fileArgs := strings.Split(string(rawArgs), "\n") 90 | if len(fileArgs) == 1 { 91 | fileArgs = strings.Split(string(rawArgs), " ") 92 | } 93 | 94 | if containsArg("-jar", fileArgs) { 95 | fileArgs = replaceJarArguments(fileArgs) 96 | newArgList := strings.Join(fileArgs, " ") 97 | if err = os.WriteFile(u.ArgumentsFile, []byte(newArgList), 0644); err != nil { 98 | return []string{}, "", fmt.Errorf("unable to write to arguments file %s\n%w", u.ArgumentsFile, err) 99 | } 100 | } 101 | 102 | inputArgs = append(inputArgs, fmt.Sprintf("@%s", u.ArgumentsFile)) 103 | 104 | return inputArgs, "", nil 105 | 106 | } 107 | 108 | // containsArg checks if needle is found in haystack 109 | // 110 | // needle and haystack entries are processed as key=val strings where only the key must match 111 | func containsArg(needle string, haystack []string) bool { 112 | needleSplit := strings.SplitN(needle, "=", 2) 113 | 114 | for _, straw := range haystack { 115 | targetSplit := strings.SplitN(straw, "=", 2) 116 | if needleSplit[0] == targetSplit[0] { 117 | return true 118 | } 119 | } 120 | 121 | return false 122 | } 123 | 124 | // ExplodedJarArguments provides a set of arguments specific to building from an exploded jar directory 125 | type ExplodedJarArguments struct { 126 | ApplicationPath string 127 | LayerPath string 128 | Manifest *properties.Properties 129 | } 130 | 131 | // NoStartOrMainClass is an error returned when a start or main class cannot be found 132 | type NoStartOrMainClass struct{} 133 | 134 | func (e NoStartOrMainClass) Error() string { 135 | return "unable to read Start-Class or Main-Class from MANIFEST.MF" 136 | } 137 | 138 | // Configure appends arguments to inputArgs for building from an exploded JAR directory 139 | func (e ExplodedJarArguments) Configure(inputArgs []string) ([]string, string, error) { 140 | startClass, ok := e.Manifest.Get("Start-Class") 141 | if !ok { 142 | startClass, ok = e.Manifest.Get("Main-Class") 143 | if !ok { 144 | return []string{}, "", NoStartOrMainClass{} 145 | } 146 | } 147 | 148 | cp := os.Getenv("CLASSPATH") 149 | if cp == "" { 150 | // CLASSPATH should have been done by upstream buildpacks, but just in case 151 | cp = e.ApplicationPath 152 | if v, ok := e.Manifest.Get("Class-Path"); ok { 153 | cp = strings.Join([]string{cp, v}, string(filepath.ListSeparator)) 154 | } 155 | } 156 | 157 | inputArgs = append(inputArgs, 158 | fmt.Sprintf("-H:Name=%s", filepath.Join(e.LayerPath, startClass)), 159 | "-cp", cp, 160 | startClass, 161 | ) 162 | 163 | return inputArgs, startClass, nil 164 | } 165 | 166 | // JarArguments provides a set of arguments specific to building from a jar file 167 | type JarArguments struct { 168 | ApplicationPath string 169 | JarFilePattern string 170 | } 171 | 172 | func (j JarArguments) Configure(inputArgs []string) ([]string, string, error) { 173 | file := filepath.Join(j.ApplicationPath, j.JarFilePattern) 174 | candidates, err := filepath.Glob(file) 175 | if err != nil { 176 | return []string{}, "", fmt.Errorf("unable to find JAR with %s\n%w", j.JarFilePattern, err) 177 | } 178 | 179 | if len(candidates) != 1 { 180 | sort.Strings(candidates) 181 | return []string{}, "", fmt.Errorf("unable to find single JAR in %s, candidates: %s", j.JarFilePattern, candidates) 182 | } 183 | 184 | jarFileName := filepath.Base(candidates[0]) 185 | startClass := strings.TrimSuffix(jarFileName, ".jar") 186 | 187 | if containsArg("-jar", inputArgs) { 188 | inputArgs = replaceJarArguments(inputArgs) 189 | } 190 | inputArgs = append(inputArgs, "-jar", candidates[0]) 191 | 192 | return inputArgs, startClass, nil 193 | } 194 | 195 | func replaceJarArguments(fileArgs []string) []string { 196 | var tmpArgs, modifiedArgs []string 197 | var skip, skipTillQuote bool 198 | var className string 199 | 200 | for i, inputArg := range fileArgs { 201 | if strings.HasPrefix(inputArg, `"`) { 202 | skipTillQuote = true 203 | continue 204 | } 205 | 206 | if skipTillQuote && strings.HasSuffix(inputArg, `"`) { 207 | skipTillQuote = false 208 | } 209 | 210 | if skip { 211 | skip = false 212 | className = strings.TrimSuffix(fileArgs[i], ".jar") 213 | continue 214 | } 215 | 216 | if inputArg == "-jar" { 217 | skip = true 218 | continue 219 | } 220 | 221 | tmpArgs = append(tmpArgs, inputArg) 222 | } 223 | 224 | for _, inputArg := range tmpArgs { 225 | if inputArg == className { 226 | continue 227 | } 228 | modifiedArgs = append(modifiedArgs, inputArg) 229 | } 230 | return modifiedArgs 231 | } 232 | -------------------------------------------------------------------------------- /native/native_image.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package native 18 | 19 | import ( 20 | "bytes" 21 | "crypto/sha256" 22 | "fmt" 23 | "os" 24 | "path/filepath" 25 | "strings" 26 | 27 | "github.com/paketo-buildpacks/native-image/v5/native/slices" 28 | 29 | "github.com/buildpacks/libcnb" 30 | "github.com/magiconair/properties" 31 | "github.com/paketo-buildpacks/libpak" 32 | "github.com/paketo-buildpacks/libpak/bard" 33 | "github.com/paketo-buildpacks/libpak/effect" 34 | "github.com/paketo-buildpacks/libpak/sherpa" 35 | ) 36 | 37 | type NativeImage struct { 38 | ApplicationPath string 39 | Arguments string 40 | ArgumentsFile string 41 | Executor effect.Executor 42 | JarFilePattern string 43 | Logger bard.Logger 44 | Manifest *properties.Properties 45 | StackID string 46 | Compressor string 47 | } 48 | 49 | func NewNativeImage(applicationPath string, arguments string, argumentsFile string, compressor string, jarFilePattern string, manifest *properties.Properties, stackID string) (NativeImage, error) { 50 | return NativeImage{ 51 | ApplicationPath: applicationPath, 52 | Arguments: arguments, 53 | ArgumentsFile: argumentsFile, 54 | Executor: effect.NewExecutor(), 55 | JarFilePattern: jarFilePattern, 56 | Manifest: manifest, 57 | StackID: stackID, 58 | Compressor: compressor, 59 | }, nil 60 | } 61 | 62 | func (n NativeImage) Contribute(layer libcnb.Layer) (libcnb.Layer, error) { 63 | files, err := sherpa.NewFileListing(n.ApplicationPath) 64 | if err != nil { 65 | return libcnb.Layer{}, fmt.Errorf("unable to create file listing for %s\n%w", n.ApplicationPath, err) 66 | } 67 | 68 | arguments, startClass, err := n.ProcessArguments(layer) 69 | if err != nil { 70 | return libcnb.Layer{}, fmt.Errorf("unable to process arguments\n%w", err) 71 | } 72 | 73 | if !slices.Contains(arguments, "--auto-fallback") && !slices.Contains(arguments, "--force-fallback") { 74 | arguments = append([]string{"--no-fallback"}, arguments...) 75 | } 76 | 77 | buf := &bytes.Buffer{} 78 | if err := n.Executor.Execute(effect.Execution{ 79 | Command: "native-image", 80 | Args: []string{"--version"}, 81 | Stdout: buf, 82 | Stderr: n.Logger.BodyWriter(), 83 | }); err != nil { 84 | return libcnb.Layer{}, fmt.Errorf("error running version\n%w", err) 85 | } 86 | nativeBinaryHash := fmt.Sprintf("%x", sha256.Sum256(buf.Bytes())) 87 | 88 | contributor := libpak.NewLayerContributor("Native Image", map[string]interface{}{ 89 | "files": files, 90 | "arguments": arguments, 91 | "compression": n.Compressor, 92 | "version-hash": nativeBinaryHash, 93 | }, libcnb.LayerTypes{ 94 | Cache: true, 95 | }) 96 | contributor.Logger = n.Logger 97 | 98 | layer, err = contributor.Contribute(layer, func() (libcnb.Layer, error) { 99 | n.Logger.Bodyf("Executing native-image %s", strings.Join(arguments, " ")) 100 | if err := n.Executor.Execute(effect.Execution{ 101 | Command: "native-image", 102 | Args: arguments, 103 | Dir: layer.Path, 104 | Stdout: n.Logger.InfoWriter(), 105 | Stderr: n.Logger.InfoWriter(), 106 | }); err != nil { 107 | return libcnb.Layer{}, fmt.Errorf("error running build\n%w", err) 108 | } 109 | 110 | if n.Compressor == CompressorUpx { 111 | n.Logger.Bodyf("Executing %s to compress native image", n.Compressor) 112 | if err := n.Executor.Execute(effect.Execution{ 113 | Command: "upx", 114 | Args: []string{"-q", "-9", filepath.Join(layer.Path, startClass)}, 115 | Dir: layer.Path, 116 | Stdout: n.Logger.InfoWriter(), 117 | Stderr: n.Logger.InfoWriter(), 118 | }); err != nil { 119 | return libcnb.Layer{}, fmt.Errorf("error compressing\n%w", err) 120 | } 121 | } else if n.Compressor == CompressorGzexe { 122 | n.Logger.Bodyf("Executing %s to compress native image", n.Compressor) 123 | if err := n.Executor.Execute(effect.Execution{ 124 | Command: "gzexe", 125 | Args: []string{filepath.Join(layer.Path, startClass)}, 126 | Dir: layer.Path, 127 | Stdout: n.Logger.InfoWriter(), 128 | Stderr: n.Logger.InfoWriter(), 129 | }); err != nil { 130 | return libcnb.Layer{}, fmt.Errorf("error compressing\n%w", err) 131 | } 132 | 133 | if err := os.Remove(filepath.Join(layer.Path, fmt.Sprintf("%s~", startClass))); err != nil { 134 | return libcnb.Layer{}, fmt.Errorf("error removing\n%w", err) 135 | } 136 | } 137 | 138 | return layer, nil 139 | }) 140 | if err != nil { 141 | return libcnb.Layer{}, fmt.Errorf("unable to contribute native-image layer\n%w", err) 142 | } 143 | 144 | n.Logger.Header("Removing bytecode") 145 | cs, err := os.ReadDir(n.ApplicationPath) 146 | if err != nil { 147 | return libcnb.Layer{}, fmt.Errorf("unable to list children of %s\n%w", n.ApplicationPath, err) 148 | } 149 | for _, c := range cs { 150 | file := filepath.Join(n.ApplicationPath, c.Name()) 151 | if err := os.RemoveAll(file); err != nil { 152 | return libcnb.Layer{}, fmt.Errorf("unable to remove %s\n%w", file, err) 153 | } 154 | } 155 | 156 | if err := copyFilesFromLayer(layer.Path, startClass, n.ApplicationPath); err != nil { 157 | return libcnb.Layer{}, fmt.Errorf("unable to copy files from layer\n%w", err) 158 | } 159 | 160 | return layer, nil 161 | } 162 | 163 | func (n NativeImage) ProcessArguments(layer libcnb.Layer) ([]string, string, error) { 164 | var arguments []string 165 | var startClass string 166 | var err error 167 | 168 | arguments, _, err = BaselineArguments{StackID: n.StackID}.Configure(nil) 169 | if err != nil { 170 | return []string{}, "", fmt.Errorf("unable to set baseline arguments\n%w", err) 171 | } 172 | 173 | if n.ArgumentsFile != "" { 174 | arguments, _, err = UserFileArguments{ArgumentsFile: n.ArgumentsFile}.Configure(arguments) 175 | if err != nil { 176 | return []string{}, "", fmt.Errorf("unable to create user file arguments\n%w", err) 177 | } 178 | } 179 | 180 | arguments, _, err = UserArguments{Arguments: n.Arguments}.Configure(arguments) 181 | if err != nil { 182 | return []string{}, "", fmt.Errorf("unable to create user arguments\n%w", err) 183 | } 184 | 185 | _, err = os.Stat(filepath.Join(n.ApplicationPath, "META-INF", "MANIFEST.MF")) 186 | if err != nil && !os.IsNotExist(err) { 187 | return []string{}, "", fmt.Errorf("unable to check for manifest\n%w", err) 188 | } else if err != nil && os.IsNotExist(err) { 189 | arguments, startClass, err = JarArguments{ 190 | ApplicationPath: n.ApplicationPath, 191 | JarFilePattern: n.JarFilePattern, 192 | }.Configure(arguments) 193 | if err != nil { 194 | return []string{}, "", fmt.Errorf("unable to append jar arguments\n%w", err) 195 | } 196 | } else { 197 | arguments, startClass, err = ExplodedJarArguments{ 198 | ApplicationPath: n.ApplicationPath, 199 | LayerPath: layer.Path, 200 | Manifest: n.Manifest, 201 | }.Configure(arguments) 202 | if err != nil { 203 | return []string{}, "", fmt.Errorf("unable to append exploded-jar directory arguments\n%w", err) 204 | } 205 | } 206 | 207 | return arguments, startClass, err 208 | } 209 | 210 | func (NativeImage) Name() string { 211 | return "native-image" 212 | } 213 | 214 | // copy the main file & any `*.so` files also in the layer to the application path 215 | func copyFilesFromLayer(layerPath string, execName string, appPath string) error { 216 | files, err := os.ReadDir(layerPath) 217 | if err != nil { 218 | return fmt.Errorf("unable to list files on layer %s\n%w", layerPath, err) 219 | } 220 | 221 | for _, file := range files { 222 | if file.Type().IsRegular() && (file.Name() == execName) { 223 | src := filepath.Join(layerPath, file.Name()) 224 | dst := filepath.Join(appPath, file.Name()) 225 | 226 | if err := copyFile(src, dst); err != nil { 227 | return fmt.Errorf("unable to copy %s to %s\n%w", src, dst, err) 228 | } 229 | } 230 | if file.Type().IsRegular() && (strings.HasSuffix(file.Name(), ".so")) { 231 | src := filepath.Join(layerPath, file.Name()) 232 | dst := filepath.Join(appPath, file.Name()) 233 | 234 | if err := copyFile(src, dst); err != nil { 235 | return fmt.Errorf("unable to copy %s to %s\n%w", src, dst, err) 236 | } 237 | } 238 | } 239 | 240 | return nil 241 | } 242 | 243 | func copyFile(src string, dst string) error { 244 | in, err := os.Open(src) 245 | if err != nil { 246 | return fmt.Errorf("unable to open %s\n%w", src, err) 247 | } 248 | defer in.Close() 249 | 250 | return sherpa.CopyFile(in, dst) 251 | } 252 | -------------------------------------------------------------------------------- /.github/workflows/pb-tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | "on": 3 | merge_group: 4 | types: 5 | - checks_requested 6 | branches: 7 | - main 8 | pull_request: {} 9 | push: 10 | branches: 11 | - main 12 | jobs: 13 | create-package: 14 | name: Create Package Test 15 | runs-on: 16 | - ubuntu-latest 17 | steps: 18 | - uses: actions/setup-go@v5 19 | with: 20 | go-version: "1.25" 21 | - name: Install create-package 22 | run: | 23 | #!/usr/bin/env bash 24 | 25 | set -euo pipefail 26 | 27 | go install -ldflags="-s -w" github.com/paketo-buildpacks/libpak/cmd/create-package@latest 28 | - uses: buildpacks/github-actions/setup-pack@v5.9.7 29 | with: 30 | pack-version: 0.39.1 31 | - name: Enable pack Experimental 32 | run: | 33 | #!/usr/bin/env bash 34 | 35 | set -euo pipefail 36 | 37 | echo "Enabling pack experimental features" 38 | pack config experimental true 39 | - uses: actions/checkout@v4 40 | - uses: actions/cache@v4 41 | with: 42 | key: ${{ runner.os }}-go-${{ hashFiles('**/buildpack.toml', '**/package.toml') }} 43 | path: |- 44 | ${{ env.HOME }}/.pack 45 | ${{ env.HOME }}/carton-cache 46 | restore-keys: ${{ runner.os }}-go- 47 | - name: Compute Version 48 | id: version 49 | run: | 50 | #!/usr/bin/env bash 51 | 52 | set -euo pipefail 53 | 54 | if [[ ${GITHUB_REF:-} != "refs/"* ]]; then 55 | echo "GITHUB_REF set to [${GITHUB_REF:-}], but that is unexpected. It should start with 'refs/*'" 56 | exit 255 57 | fi 58 | 59 | if [[ ${GITHUB_REF} =~ refs/tags/v([0-9]+\.[0-9]+\.[0-9]+) ]]; then 60 | VERSION=${BASH_REMATCH[1]} 61 | 62 | MAJOR_VERSION="$(echo "${VERSION}" | awk -F '.' '{print $1 }')" 63 | MINOR_VERSION="$(echo "${VERSION}" | awk -F '.' '{print $1 "." $2 }')" 64 | 65 | echo "version-major=${MAJOR_VERSION}" >> "$GITHUB_OUTPUT" 66 | echo "version-minor=${MINOR_VERSION}" >> "$GITHUB_OUTPUT" 67 | elif [[ ${GITHUB_REF} =~ refs/heads/(.+) ]]; then 68 | VERSION=${BASH_REMATCH[1]} 69 | else 70 | VERSION=$(git rev-parse --short HEAD) 71 | fi 72 | 73 | echo "version=${VERSION}" >> "$GITHUB_OUTPUT" 74 | echo "Selected ${VERSION} from 75 | * ref: ${GITHUB_REF} 76 | * sha: ${GITHUB_SHA} 77 | " 78 | - name: Create Package 79 | run: | 80 | #!/usr/bin/env bash 81 | 82 | set -euo pipefail 83 | 84 | # With Go 1.20, we need to set this so that we produce statically compiled binaries 85 | # 86 | # Starting with Go 1.20, Go will produce binaries that are dynamically linked against libc 87 | # which can cause compatibility issues. The compiler links against libc on the build system 88 | # but that may be newer than on the stacks we support. 89 | export CGO_ENABLED=0 90 | 91 | if [[ "${INCLUDE_DEPENDENCIES}" == "true" ]]; then 92 | create-package \ 93 | --source "${SOURCE_PATH:-.}" \ 94 | --cache-location "${HOME}"/carton-cache \ 95 | --destination "${HOME}"/buildpack \ 96 | --include-dependencies \ 97 | --version "${VERSION}" 98 | else 99 | create-package \ 100 | --source "${SOURCE_PATH:-.}" \ 101 | --destination "${HOME}"/buildpack \ 102 | --version "${VERSION}" 103 | fi 104 | 105 | PACKAGE_FILE="${SOURCE_PATH:-.}/package.toml" 106 | if [ -f "${PACKAGE_FILE}" ]; then 107 | cp "${PACKAGE_FILE}" "${HOME}/buildpack/package.toml" 108 | printf '[buildpack]\nuri = "%s"\n\n[platform]\nos = "%s"\n' "${HOME}/buildpack" "${OS}" >> "${HOME}/buildpack/package.toml" 109 | fi 110 | env: 111 | INCLUDE_DEPENDENCIES: "true" 112 | OS: linux 113 | VERSION: ${{ steps.version.outputs.version }} 114 | - name: Package Buildpack 115 | run: |- 116 | #!/usr/bin/env bash 117 | 118 | set -euo pipefail 119 | 120 | COMPILED_BUILDPACK="${HOME}/buildpack" 121 | 122 | # create-package puts the buildpack here, we need to run from that directory 123 | # for component buildpacks so that pack doesn't need a package.toml 124 | cd "${COMPILED_BUILDPACK}" 125 | CONFIG="" 126 | if [ -f "${COMPILED_BUILDPACK}/package.toml" ]; then 127 | CONFIG="--config ${COMPILED_BUILDPACK}/package.toml --flatten" 128 | fi 129 | 130 | PACKAGE_LIST=($PACKAGES) 131 | # Extract first repo (Docker Hub) as the main to package & register 132 | PACKAGE=${PACKAGE_LIST[0]} 133 | 134 | if [[ "${PUBLISH:-x}" == "true" ]]; then 135 | pack -v buildpack package \ 136 | "${PACKAGE}:${VERSION}" ${CONFIG} \ 137 | --publish 138 | 139 | if [[ -n ${VERSION_MINOR:-} && -n ${VERSION_MAJOR:-} ]]; then 140 | crane tag "${PACKAGE}:${VERSION}" "${VERSION_MINOR}" 141 | crane tag "${PACKAGE}:${VERSION}" "${VERSION_MAJOR}" 142 | fi 143 | crane tag "${PACKAGE}:${VERSION}" latest 144 | echo "digest=$(crane digest "${PACKAGE}:${VERSION}")" >> "$GITHUB_OUTPUT" 145 | 146 | # copy to other repositories specified 147 | for P in "${PACKAGE_LIST[@]}" 148 | do 149 | if [ "$P" != "$PACKAGE" ]; then 150 | crane copy "${PACKAGE}:${VERSION}" "${P}:${VERSION}" 151 | if [[ -n ${VERSION_MINOR:-} && -n ${VERSION_MAJOR:-} ]]; then 152 | crane tag "${P}:${VERSION}" "${VERSION_MINOR}" 153 | crane tag "${P}:${VERSION}" "${VERSION_MAJOR}" 154 | fi 155 | crane tag "${P}:${VERSION}" latest 156 | fi 157 | done 158 | else 159 | if [ -n "$TTL_SH_PUBLISH" ] && [ "$TTL_SH_PUBLISH" = "true" ]; then 160 | TAG="${PACKAGE}-$(mktemp -u XXXXX | awk '{print tolower($0)}'):${VERSION}" 161 | pack -v buildpack package "${TAG}" ${CONFIG} --format "${FORMAT}" --publish 162 | else 163 | TAG="${PACKAGE}:${VERSION}" 164 | pack -v buildpack package "${TAG}" ${CONFIG} --format "${FORMAT}" 165 | fi 166 | 167 | echo "ttl-image-tag=${TAG:-}" >> "$GITHUB_OUTPUT" 168 | fi 169 | env: 170 | FORMAT: image 171 | PACKAGES: test 172 | TTL_SH_PUBLISH: "false" 173 | VERSION: ${{ steps.version.outputs.version }} 174 | unit: 175 | name: Unit Test 176 | runs-on: 177 | - ubuntu-latest 178 | steps: 179 | - uses: actions/checkout@v4 180 | - uses: actions/cache@v4 181 | with: 182 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 183 | path: ${{ env.HOME }}/go/pkg/mod 184 | restore-keys: ${{ runner.os }}-go- 185 | - uses: actions/setup-go@v5 186 | with: 187 | go-version: "1.25" 188 | - name: Install richgo 189 | run: | 190 | #!/usr/bin/env bash 191 | 192 | set -euo pipefail 193 | 194 | echo "Installing richgo ${RICHGO_VERSION}" 195 | 196 | mkdir -p "${HOME}"/bin 197 | echo "${HOME}/bin" >> "${GITHUB_PATH}" 198 | 199 | curl \ 200 | --location \ 201 | --show-error \ 202 | --silent \ 203 | "https://github.com/kyoh86/richgo/releases/download/v${RICHGO_VERSION}/richgo_${RICHGO_VERSION}_linux_amd64.tar.gz" \ 204 | | tar -C "${HOME}"/bin -xz richgo 205 | env: 206 | RICHGO_VERSION: 0.3.10 207 | - name: Run Tests 208 | run: | 209 | #!/usr/bin/env bash 210 | 211 | set -euo pipefail 212 | 213 | richgo test ./... 214 | env: 215 | RICHGO_FORCE_COLOR: "1" 216 | -------------------------------------------------------------------------------- /.github/workflows/pb-create-package.yml: -------------------------------------------------------------------------------- 1 | name: Create Package 2 | "on": 3 | release: 4 | types: 5 | - published 6 | jobs: 7 | create-package: 8 | name: Create Package 9 | runs-on: 10 | - ubuntu-latest 11 | steps: 12 | - name: Docker login docker.io 13 | if: ${{ (github.event_name != 'pull_request' || ! github.event.pull_request.head.repo.fork) && (github.actor != 'dependabot[bot]') }} 14 | uses: docker/login-action@v3 15 | with: 16 | password: ${{ secrets.PAKETO_BUILDPACKS_DOCKERHUB_PASSWORD }} 17 | registry: docker.io 18 | username: ${{ secrets.PAKETO_BUILDPACKS_DOCKERHUB_USERNAME }} 19 | - uses: actions/setup-go@v5 20 | with: 21 | go-version: "1.25" 22 | - name: Install create-package 23 | run: | 24 | #!/usr/bin/env bash 25 | 26 | set -euo pipefail 27 | 28 | go install -ldflags="-s -w" github.com/paketo-buildpacks/libpak/cmd/create-package@latest 29 | - uses: buildpacks/github-actions/setup-tools@v5.9.7 30 | with: 31 | crane-version: 0.20.3 32 | yj-version: 5.1.0 33 | - uses: buildpacks/github-actions/setup-pack@v5.9.7 34 | with: 35 | pack-version: 0.39.1 36 | - name: Enable pack Experimental 37 | run: | 38 | #!/usr/bin/env bash 39 | 40 | set -euo pipefail 41 | 42 | echo "Enabling pack experimental features" 43 | pack config experimental true 44 | - uses: actions/checkout@v4 45 | - if: ${{ false }} 46 | uses: actions/cache@v4 47 | with: 48 | key: ${{ runner.os }}-go-${{ hashFiles('**/buildpack.toml', '**/package.toml') }} 49 | path: |- 50 | ${{ env.HOME }}/.pack 51 | ${{ env.HOME }}/carton-cache 52 | restore-keys: ${{ runner.os }}-go- 53 | - name: Compute Version 54 | id: version 55 | run: | 56 | #!/usr/bin/env bash 57 | 58 | set -euo pipefail 59 | 60 | if [[ ${GITHUB_REF:-} != "refs/"* ]]; then 61 | echo "GITHUB_REF set to [${GITHUB_REF:-}], but that is unexpected. It should start with 'refs/*'" 62 | exit 255 63 | fi 64 | 65 | if [[ ${GITHUB_REF} =~ refs/tags/v([0-9]+\.[0-9]+\.[0-9]+) ]]; then 66 | VERSION=${BASH_REMATCH[1]} 67 | 68 | MAJOR_VERSION="$(echo "${VERSION}" | awk -F '.' '{print $1 }')" 69 | MINOR_VERSION="$(echo "${VERSION}" | awk -F '.' '{print $1 "." $2 }')" 70 | 71 | echo "version-major=${MAJOR_VERSION}" >> "$GITHUB_OUTPUT" 72 | echo "version-minor=${MINOR_VERSION}" >> "$GITHUB_OUTPUT" 73 | elif [[ ${GITHUB_REF} =~ refs/heads/(.+) ]]; then 74 | VERSION=${BASH_REMATCH[1]} 75 | else 76 | VERSION=$(git rev-parse --short HEAD) 77 | fi 78 | 79 | echo "version=${VERSION}" >> "$GITHUB_OUTPUT" 80 | echo "Selected ${VERSION} from 81 | * ref: ${GITHUB_REF} 82 | * sha: ${GITHUB_SHA} 83 | " 84 | - name: Create Package 85 | run: | 86 | #!/usr/bin/env bash 87 | 88 | set -euo pipefail 89 | 90 | # With Go 1.20, we need to set this so that we produce statically compiled binaries 91 | # 92 | # Starting with Go 1.20, Go will produce binaries that are dynamically linked against libc 93 | # which can cause compatibility issues. The compiler links against libc on the build system 94 | # but that may be newer than on the stacks we support. 95 | export CGO_ENABLED=0 96 | 97 | if [[ "${INCLUDE_DEPENDENCIES}" == "true" ]]; then 98 | create-package \ 99 | --source "${SOURCE_PATH:-.}" \ 100 | --cache-location "${HOME}"/carton-cache \ 101 | --destination "${HOME}"/buildpack \ 102 | --include-dependencies \ 103 | --version "${VERSION}" 104 | else 105 | create-package \ 106 | --source "${SOURCE_PATH:-.}" \ 107 | --destination "${HOME}"/buildpack \ 108 | --version "${VERSION}" 109 | fi 110 | 111 | PACKAGE_FILE="${SOURCE_PATH:-.}/package.toml" 112 | if [ -f "${PACKAGE_FILE}" ]; then 113 | cp "${PACKAGE_FILE}" "${HOME}/buildpack/package.toml" 114 | printf '[buildpack]\nuri = "%s"\n\n[platform]\nos = "%s"\n' "${HOME}/buildpack" "${OS}" >> "${HOME}/buildpack/package.toml" 115 | fi 116 | env: 117 | INCLUDE_DEPENDENCIES: "false" 118 | OS: linux 119 | SOURCE_PATH: "" 120 | VERSION: ${{ steps.version.outputs.version }} 121 | - name: Package Buildpack 122 | id: package 123 | run: |- 124 | #!/usr/bin/env bash 125 | 126 | set -euo pipefail 127 | 128 | COMPILED_BUILDPACK="${HOME}/buildpack" 129 | 130 | # create-package puts the buildpack here, we need to run from that directory 131 | # for component buildpacks so that pack doesn't need a package.toml 132 | cd "${COMPILED_BUILDPACK}" 133 | CONFIG="" 134 | if [ -f "${COMPILED_BUILDPACK}/package.toml" ]; then 135 | CONFIG="--config ${COMPILED_BUILDPACK}/package.toml --flatten" 136 | fi 137 | 138 | PACKAGE_LIST=($PACKAGES) 139 | # Extract first repo (Docker Hub) as the main to package & register 140 | PACKAGE=${PACKAGE_LIST[0]} 141 | 142 | if [[ "${PUBLISH:-x}" == "true" ]]; then 143 | pack -v buildpack package \ 144 | "${PACKAGE}:${VERSION}" ${CONFIG} \ 145 | --publish 146 | 147 | if [[ -n ${VERSION_MINOR:-} && -n ${VERSION_MAJOR:-} ]]; then 148 | crane tag "${PACKAGE}:${VERSION}" "${VERSION_MINOR}" 149 | crane tag "${PACKAGE}:${VERSION}" "${VERSION_MAJOR}" 150 | fi 151 | crane tag "${PACKAGE}:${VERSION}" latest 152 | echo "digest=$(crane digest "${PACKAGE}:${VERSION}")" >> "$GITHUB_OUTPUT" 153 | 154 | # copy to other repositories specified 155 | for P in "${PACKAGE_LIST[@]}" 156 | do 157 | if [ "$P" != "$PACKAGE" ]; then 158 | crane copy "${PACKAGE}:${VERSION}" "${P}:${VERSION}" 159 | if [[ -n ${VERSION_MINOR:-} && -n ${VERSION_MAJOR:-} ]]; then 160 | crane tag "${P}:${VERSION}" "${VERSION_MINOR}" 161 | crane tag "${P}:${VERSION}" "${VERSION_MAJOR}" 162 | fi 163 | crane tag "${P}:${VERSION}" latest 164 | fi 165 | done 166 | else 167 | if [ -n "$TTL_SH_PUBLISH" ] && [ "$TTL_SH_PUBLISH" = "true" ]; then 168 | TAG="${PACKAGE}-$(mktemp -u XXXXX | awk '{print tolower($0)}'):${VERSION}" 169 | pack -v buildpack package "${TAG}" ${CONFIG} --format "${FORMAT}" --publish 170 | else 171 | TAG="${PACKAGE}:${VERSION}" 172 | pack -v buildpack package "${TAG}" ${CONFIG} --format "${FORMAT}" 173 | fi 174 | 175 | echo "ttl-image-tag=${TAG:-}" >> "$GITHUB_OUTPUT" 176 | fi 177 | env: 178 | PACKAGES: docker.io/paketobuildpacks/native-image 179 | PUBLISH: "true" 180 | VERSION: ${{ steps.version.outputs.version }} 181 | VERSION_MAJOR: ${{ steps.version.outputs.version-major }} 182 | VERSION_MINOR: ${{ steps.version.outputs.version-minor }} 183 | - name: Update release with digest 184 | run: | 185 | #!/usr/bin/env bash 186 | 187 | set -euo pipefail 188 | 189 | PAYLOAD=$(cat "${GITHUB_EVENT_PATH}") 190 | 191 | RELEASE_ID=$(jq -n -r --argjson PAYLOAD "${PAYLOAD}" '$PAYLOAD.release.id') 192 | RELEASE_TAG_NAME=$(jq -n -r --argjson PAYLOAD "${PAYLOAD}" '$PAYLOAD.release.tag_name') 193 | RELEASE_NAME=$(jq -n -r --argjson PAYLOAD "${PAYLOAD}" '$PAYLOAD.release.name') 194 | RELEASE_BODY=$(jq -n -r --argjson PAYLOAD "${PAYLOAD}" '$PAYLOAD.release.body') 195 | 196 | gh api \ 197 | --method PATCH \ 198 | "/repos/:owner/:repo/releases/${RELEASE_ID}" \ 199 | --field "tag_name=${RELEASE_TAG_NAME}" \ 200 | --field "name=${RELEASE_NAME}" \ 201 | --field "body=${RELEASE_BODY///\`${DIGEST}\`}" 202 | env: 203 | DIGEST: ${{ steps.package.outputs.digest }} 204 | GITHUB_TOKEN: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }} 205 | - if: ${{ true }} 206 | uses: docker://ghcr.io/buildpacks/actions/registry/request-add-entry:5.9.7 207 | with: 208 | address: docker.io/paketobuildpacks/native-image@${{ steps.package.outputs.digest }} 209 | id: paketo-buildpacks/native-image 210 | token: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }} 211 | version: ${{ steps.version.outputs.version }} 212 | -------------------------------------------------------------------------------- /native/build_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package native_test 18 | 19 | import ( 20 | "bytes" 21 | "os" 22 | "path/filepath" 23 | "testing" 24 | 25 | "github.com/paketo-buildpacks/libpak/sbom/mocks" 26 | "github.com/paketo-buildpacks/libpak/sherpa" 27 | 28 | "github.com/buildpacks/libcnb" 29 | . "github.com/onsi/gomega" 30 | "github.com/paketo-buildpacks/libpak/bard" 31 | "github.com/sclevine/spec" 32 | 33 | "github.com/paketo-buildpacks/native-image/v5/native" 34 | ) 35 | 36 | func testBuild(t *testing.T, context spec.G, it spec.S) { 37 | var ( 38 | Expect = NewWithT(t).Expect 39 | 40 | ctx libcnb.BuildContext 41 | build native.Build 42 | out bytes.Buffer 43 | sbomScanner mocks.SBOMScanner 44 | ) 45 | 46 | it.Before(func() { 47 | ctx.Application.Path = t.TempDir() 48 | ctx.Layers.Path = t.TempDir() 49 | 50 | sbomScanner = mocks.SBOMScanner{} 51 | sbomScanner.On("ScanLaunch", ctx.Application.Path, libcnb.SyftJSON, libcnb.CycloneDXJSON).Return(nil) 52 | 53 | build.Logger = bard.NewLogger(&out) 54 | build.SBOMScanner = &sbomScanner 55 | 56 | Expect(os.MkdirAll(filepath.Join(ctx.Application.Path, "META-INF"), 0755)).To(Succeed()) 57 | 58 | ctx.Buildpack.Metadata = map[string]interface{}{ 59 | "dependencies": []map[string]interface{}{ 60 | { 61 | "id": "spring-graalvm-native", 62 | "version": "1.1.1", 63 | "stacks": []interface{}{"test-stack-id"}, 64 | }, 65 | }, 66 | } 67 | 68 | ctx.StackID = "test-stack-id" 69 | }) 70 | 71 | it.After(func() { 72 | Expect(os.RemoveAll(ctx.Application.Path)).To(Succeed()) 73 | Expect(os.RemoveAll(ctx.Layers.Path)).To(Succeed()) 74 | }) 75 | 76 | it("contributes native image layer", func() { 77 | Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "META-INF", "MANIFEST.MF"), []byte(` 78 | Spring-Boot-Version: 1.1.1 79 | Spring-Boot-Classes: BOOT-INF/classes 80 | Spring-Boot-Lib: BOOT-INF/lib 81 | Spring-Boot-Layers-Index: layers.idx 82 | Start-Class: test-start-class 83 | `), 0644)).To(Succeed()) 84 | 85 | result, err := build.Build(ctx) 86 | Expect(err).NotTo(HaveOccurred()) 87 | 88 | Expect(result.Layers).To(HaveLen(1)) 89 | Expect(result.Layers[0].(native.NativeImage).Arguments).To(BeEmpty()) 90 | Expect(result.Processes).To(ContainElements( 91 | libcnb.Process{Type: "native-image", Command: "./test-start-class", Direct: true}, 92 | libcnb.Process{Type: "task", Command: "./test-start-class", Direct: true}, 93 | libcnb.Process{Type: "web", Command: "./test-start-class", Direct: true, Default: true}, 94 | )) 95 | sbomScanner.AssertCalled(t, "ScanLaunch", ctx.Application.Path, libcnb.SyftJSON, libcnb.CycloneDXJSON) 96 | }) 97 | 98 | context("BP_NATIVE_IMAGE", func() { 99 | context("when true", func() { 100 | it.Before(func() { 101 | t.Setenv("BP_NATIVE_IMAGE", "true") 102 | }) 103 | 104 | it("contributes native image layer", func() { 105 | Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "META-INF", "MANIFEST.MF"), []byte(` 106 | Spring-Boot-Version: 1.1.1 107 | Spring-Boot-Classes: BOOT-INF/classes 108 | Spring-Boot-Lib: BOOT-INF/lib 109 | Spring-Boot-Layers-Index: layers.idx 110 | Start-Class: test-start-class 111 | `), 0644)).To(Succeed()) 112 | 113 | result, err := build.Build(ctx) 114 | Expect(err).NotTo(HaveOccurred()) 115 | 116 | Expect(result.Layers).To(HaveLen(1)) 117 | Expect(result.Layers[0].(native.NativeImage).Arguments).To(BeEmpty()) 118 | Expect(result.Processes).To(ContainElements( 119 | libcnb.Process{Type: "native-image", Command: "./test-start-class", Direct: true}, 120 | libcnb.Process{Type: "task", Command: "./test-start-class", Direct: true}, 121 | libcnb.Process{Type: "web", Command: "./test-start-class", Direct: true, Default: true}, 122 | )) 123 | 124 | sbomScanner.AssertCalled(t, "ScanLaunch", ctx.Application.Path, libcnb.SyftJSON, libcnb.CycloneDXJSON) 125 | }) 126 | }) 127 | 128 | context("when false", func() { 129 | it.Before(func() { 130 | t.Setenv("BP_NATIVE_IMAGE", "false") 131 | }) 132 | 133 | it("does nothing and skips build", func() { 134 | Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "META-INF", "MANIFEST.MF"), []byte(` 135 | Spring-Boot-Version: 1.1.1 136 | Spring-Boot-Classes: BOOT-INF/classes 137 | Spring-Boot-Lib: BOOT-INF/lib 138 | Spring-Boot-Layers-Index: layers.idx 139 | Start-Class: test-start-class 140 | `), 0644)).To(Succeed()) 141 | 142 | result, err := build.Build(ctx) 143 | Expect(err).NotTo(HaveOccurred()) 144 | 145 | Expect(result.Layers).To(HaveLen(0)) 146 | Expect(result.Processes).To(BeEmpty()) 147 | 148 | sbomScanner.AssertNotCalled(t, "ScanLaunch", ctx.Application.Path, libcnb.SyftJSON, libcnb.CycloneDXJSON) 149 | }) 150 | }) 151 | }) 152 | 153 | context("BP_BOOT_NATIVE_IMAGE", func() { 154 | it.Before(func() { 155 | Expect(os.Setenv("BP_BOOT_NATIVE_IMAGE", "true")).To(Succeed()) 156 | }) 157 | 158 | it.After(func() { 159 | Expect(os.Unsetenv("BP_BOOT_NATIVE_IMAGE")).To(Succeed()) 160 | }) 161 | 162 | it("contributes native image layer and prints a deprecation warning", func() { 163 | Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "META-INF", "MANIFEST.MF"), []byte(` 164 | Spring-Boot-Version: 1.1.1 165 | Spring-Boot-Classes: BOOT-INF/classes 166 | Spring-Boot-Lib: BOOT-INF/lib 167 | Spring-Boot-Layers-Index: layers.idx 168 | Start-Class: test-start-class 169 | `), 0644)).To(Succeed()) 170 | 171 | result, err := build.Build(ctx) 172 | Expect(err).NotTo(HaveOccurred()) 173 | 174 | Expect(result.Layers).To(HaveLen(1)) 175 | Expect(result.Layers[0].(native.NativeImage).Arguments).To(BeEmpty()) 176 | Expect(result.Processes).To(ContainElements( 177 | libcnb.Process{Type: "native-image", Command: "./test-start-class", Direct: true}, 178 | libcnb.Process{Type: "task", Command: "./test-start-class", Direct: true}, 179 | libcnb.Process{Type: "web", Command: "./test-start-class", Direct: true, Default: true}, 180 | )) 181 | 182 | Expect(out.String()).To(ContainSubstring("$BP_BOOT_NATIVE_IMAGE has been deprecated. Please use $BP_NATIVE_IMAGE instead.")) 183 | sbomScanner.AssertCalled(t, "ScanLaunch", ctx.Application.Path, libcnb.SyftJSON, libcnb.CycloneDXJSON) 184 | }) 185 | }) 186 | 187 | context("BP_NATIVE_IMAGE_BUILD_ARGUMENTS", func() { 188 | it.Before(func() { 189 | Expect(os.Setenv("BP_NATIVE_IMAGE_BUILD_ARGUMENTS", "test-native-image-argument")).To(Succeed()) 190 | }) 191 | 192 | it.After(func() { 193 | Expect(os.Unsetenv("BP_NATIVE_IMAGE_BUILD_ARGUMENTS")).To(Succeed()) 194 | }) 195 | 196 | it("contributes native image build arguments", func() { 197 | Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "META-INF", "MANIFEST.MF"), []byte(` 198 | Spring-Boot-Version: 1.1.1 199 | Spring-Boot-Classes: BOOT-INF/classes 200 | Spring-Boot-Lib: BOOT-INF/lib 201 | Spring-Boot-Layers-Index: layers.idx 202 | Start-Class: test-start-class 203 | `), 0644)).To(Succeed()) 204 | 205 | result, err := build.Build(ctx) 206 | Expect(err).NotTo(HaveOccurred()) 207 | 208 | Expect(result.Layers[0].(native.NativeImage).Arguments).To(Equal("test-native-image-argument")) 209 | }) 210 | }) 211 | 212 | context("BP_BOOT_NATIVE_IMAGE_BUILD_ARGUMENTS", func() { 213 | it.Before(func() { 214 | Expect(os.Setenv("BP_BOOT_NATIVE_IMAGE_BUILD_ARGUMENTS", "test-native-image-argument")).To(Succeed()) 215 | }) 216 | 217 | it.After(func() { 218 | Expect(os.Unsetenv("BP_BOOT_NATIVE_IMAGE_BUILD_ARGUMENTS")).To(Succeed()) 219 | }) 220 | 221 | it("contributes native image build arguments and prints a deprecation warning", func() { 222 | Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "META-INF", "MANIFEST.MF"), []byte(` 223 | Spring-Boot-Version: 1.1.1 224 | Spring-Boot-Classes: BOOT-INF/classes 225 | Spring-Boot-Lib: BOOT-INF/lib 226 | Spring-Boot-Layers-Index: layers.idx 227 | Start-Class: test-start-class 228 | `), 0644)).To(Succeed()) 229 | 230 | result, err := build.Build(ctx) 231 | Expect(err).NotTo(HaveOccurred()) 232 | 233 | Expect(result.Layers[0].(native.NativeImage).Arguments).To(Equal("test-native-image-argument")) 234 | 235 | Expect(out.String()).To(ContainSubstring("$BP_BOOT_NATIVE_IMAGE_BUILD_ARGUMENTS has been deprecated. Please use $BP_NATIVE_IMAGE_BUILD_ARGUMENTS instead.")) 236 | }) 237 | }) 238 | 239 | context("BP_NATIVE_IMAGE_BUILT_ARTIFACT", func() { 240 | it.Before(func() { 241 | Expect(os.Setenv("BP_NATIVE_IMAGE_BUILT_ARTIFACT", "target/*.jar")).To(Succeed()) 242 | }) 243 | 244 | it.After(func() { 245 | Expect(os.Unsetenv("BP_NATIVE_IMAGE_BUILT_ARTIFACT")).To(Succeed()) 246 | }) 247 | 248 | it("contributes native image layer to build against a JAR", func() { 249 | Expect(os.MkdirAll(filepath.Join(ctx.Application.Path, "target"), 0755)).To(Succeed()) 250 | 251 | fp, err := os.Open("testdata/test-fixture.jar") 252 | Expect(err).ToNot(HaveOccurred()) 253 | Expect(sherpa.CopyFile(fp, filepath.Join(ctx.Application.Path, "target", "test-fixture.jar"))).To(Succeed()) 254 | 255 | result, err := build.Build(ctx) 256 | Expect(err).NotTo(HaveOccurred()) 257 | 258 | Expect(result.Layers[0].(native.NativeImage).JarFilePattern).To(Equal("target/*.jar")) 259 | Expect(result.Processes).To(ContainElements( 260 | libcnb.Process{Type: "native-image", Command: "./test-fixture", Direct: true}, 261 | libcnb.Process{Type: "task", Command: "./test-fixture", Direct: true}, 262 | libcnb.Process{Type: "web", Command: "./test-fixture", Direct: true, Default: true}, 263 | )) 264 | }) 265 | }) 266 | } 267 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | https://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | https://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /native/arguments_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package native_test 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "path/filepath" 23 | "testing" 24 | 25 | "github.com/buildpacks/libcnb" 26 | "github.com/magiconair/properties" 27 | . "github.com/onsi/gomega" 28 | "github.com/paketo-buildpacks/libpak" 29 | "github.com/paketo-buildpacks/native-image/v5/native" 30 | "github.com/sclevine/spec" 31 | ) 32 | 33 | func testArguments(t *testing.T, context spec.G, it spec.S) { 34 | var ( 35 | Expect = NewWithT(t).Expect 36 | 37 | ctx libcnb.BuildContext 38 | props *properties.Properties 39 | ) 40 | 41 | it.Before(func() { 42 | ctx.Application.Path = t.TempDir() 43 | ctx.Layers.Path = t.TempDir() 44 | }) 45 | 46 | it.After(func() { 47 | Expect(os.RemoveAll(ctx.Application.Path)).To(Succeed()) 48 | Expect(os.RemoveAll(ctx.Layers.Path)).To(Succeed()) 49 | }) 50 | 51 | context("baseline arguments", func() { 52 | it("sets default arguments", func() { 53 | args, startClass, err := native.BaselineArguments{}.Configure(nil) 54 | Expect(err).ToNot(HaveOccurred()) 55 | Expect(startClass).To(Equal("")) 56 | Expect(args).To(HaveLen(0)) 57 | }) 58 | 59 | it("ignores input arguments", func() { 60 | inputArgs := []string{"one", "two", "three"} 61 | args, startClass, err := native.BaselineArguments{}.Configure(inputArgs) 62 | Expect(err).ToNot(HaveOccurred()) 63 | Expect(startClass).To(Equal("")) 64 | Expect(args).To(HaveLen(0)) 65 | }) 66 | 67 | it("sets defaults for tiny stack", func() { 68 | args, startClass, err := native.BaselineArguments{StackID: libpak.TinyStackID}.Configure(nil) 69 | Expect(err).ToNot(HaveOccurred()) 70 | Expect(startClass).To(Equal("")) 71 | Expect(args).To(HaveLen(1)) 72 | Expect(args).To(Equal([]string{"-H:+StaticExecutableWithDynamicLibC"})) 73 | }) 74 | }) 75 | 76 | context("user arguments", func() { 77 | it("has none", func() { 78 | inputArgs := []string{"one", "two", "three"} 79 | args, startClass, err := native.UserArguments{}.Configure(inputArgs) 80 | Expect(err).ToNot(HaveOccurred()) 81 | Expect(startClass).To(Equal("")) 82 | Expect(args).To(HaveLen(3)) 83 | Expect(args).To(Equal([]string{"one", "two", "three"})) 84 | }) 85 | 86 | it("has some and appends to end", func() { 87 | inputArgs := []string{"one", "two", "three"} 88 | args, startClass, err := native.UserArguments{ 89 | Arguments: "more stuff", 90 | }.Configure(inputArgs) 91 | Expect(err).ToNot(HaveOccurred()) 92 | Expect(startClass).To(Equal("")) 93 | Expect(args).To(HaveLen(5)) 94 | Expect(args).To(Equal([]string{"one", "two", "three", "more", "stuff"})) 95 | }) 96 | 97 | it("works with quotes", func() { 98 | inputArgs := []string{"one", "two", "three"} 99 | args, startClass, err := native.UserArguments{ 100 | Arguments: `"more stuff"`, 101 | }.Configure(inputArgs) 102 | Expect(err).ToNot(HaveOccurred()) 103 | Expect(startClass).To(Equal("")) 104 | Expect(args).To(HaveLen(4)) 105 | Expect(args).To(Equal([]string{"one", "two", "three", "more stuff"})) 106 | }) 107 | 108 | it("allows a user argument to override an input argument", func() { 109 | inputArgs := []string{"one=input", "two", "three"} 110 | args, startClass, err := native.UserArguments{ 111 | Arguments: `one=output`, 112 | }.Configure(inputArgs) 113 | Expect(err).ToNot(HaveOccurred()) 114 | Expect(startClass).To(Equal("")) 115 | Expect(args).To(HaveLen(3)) 116 | Expect(args).To(Equal([]string{"two", "three", "one=output"})) 117 | }) 118 | }) 119 | 120 | context("user arguments from file", func() { 121 | it.Before(func() { 122 | Expect(os.MkdirAll(filepath.Join(ctx.Application.Path, "target"), 0755)).To(Succeed()) 123 | Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "target", "more-stuff.txt"), []byte("more stuff"), 0644)).To(Succeed()) 124 | Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "target", "more-stuff-quotes.txt"), []byte(`before -jar "more stuff.jar" after -other="my path"`), 0644)).To(Succeed()) 125 | Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "target", "more-stuff-class.txt"), []byte(`stuff -jar stuff.jar after`), 0644)).To(Succeed()) 126 | Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "target", "override.txt"), []byte(`one=output`), 0644)).To(Succeed()) 127 | }) 128 | 129 | it("has none", func() { 130 | inputArgs := []string{"one", "two", "three"} 131 | _, _, err := native.UserFileArguments{}.Configure(inputArgs) 132 | Expect(err).To(MatchError(os.ErrNotExist)) 133 | }) 134 | 135 | it("has some and appends to end", func() { 136 | inputArgs := []string{"one", "two", "three"} 137 | args, startClass, err := native.UserFileArguments{ 138 | ArgumentsFile: filepath.Join(ctx.Application.Path, "target/more-stuff.txt"), 139 | }.Configure(inputArgs) 140 | Expect(err).ToNot(HaveOccurred()) 141 | Expect(startClass).To(Equal("")) 142 | Expect(args).To(HaveLen(4)) 143 | Expect(args).To(Equal([]string{"one", "two", "three", fmt.Sprintf("@%s", filepath.Join(ctx.Application.Path, "target/more-stuff.txt"))})) 144 | }) 145 | 146 | it("works with quotes in the file", func() { 147 | inputArgs := []string{"one", "two", "three"} 148 | args, startClass, err := native.UserFileArguments{ 149 | ArgumentsFile: filepath.Join(ctx.Application.Path, "target/more-stuff-quotes.txt"), 150 | }.Configure(inputArgs) 151 | Expect(err).ToNot(HaveOccurred()) 152 | Expect(startClass).To(Equal("")) 153 | Expect(args).To(HaveLen(4)) 154 | Expect(args).To(Equal([]string{"one", "two", "three", fmt.Sprintf("@%s", filepath.Join(ctx.Application.Path, "target/more-stuff-quotes.txt"))})) 155 | bits, err := os.ReadFile(filepath.Join(ctx.Application.Path, "target/more-stuff-quotes.txt")) 156 | Expect(err).ToNot(HaveOccurred()) 157 | Expect(string(bits)).To(Equal("before after -other=\"my path\"")) 158 | }) 159 | 160 | it("removes the class name argument if found", func() { 161 | args, _, err := native.UserFileArguments{ 162 | ArgumentsFile: filepath.Join(ctx.Application.Path, "target/more-stuff-class.txt"), 163 | }.Configure(nil) 164 | Expect(err).ToNot(HaveOccurred()) 165 | Expect(args).To(HaveLen(1)) 166 | Expect(args).To(Equal([]string{ 167 | fmt.Sprintf("@%s", filepath.Join(ctx.Application.Path, "target", "more-stuff-class.txt")), 168 | })) 169 | bits, err := os.ReadFile(filepath.Join(ctx.Application.Path, "target/more-stuff-class.txt")) 170 | Expect(err).ToNot(HaveOccurred()) 171 | Expect(string(bits)).To(Equal("after")) 172 | }) 173 | }) 174 | 175 | context("exploded jar arguments", func() { 176 | var layer libcnb.Layer 177 | 178 | it.Before(func() { 179 | var err error 180 | 181 | layer, err = ctx.Layers.Layer("test-layer") 182 | Expect(err).NotTo(HaveOccurred()) 183 | 184 | props = properties.NewProperties() 185 | _, _, err = props.Set("Start-Class", "test-start-class") 186 | Expect(err).NotTo(HaveOccurred()) 187 | _, _, err = props.Set("Class-Path", "manifest-class-path") 188 | Expect(err).NotTo(HaveOccurred()) 189 | }) 190 | 191 | it("adds arguments, no CLASSPATH set", func() { 192 | inputArgs := []string{"stuff"} 193 | args, startClass, err := native.ExplodedJarArguments{ 194 | ApplicationPath: ctx.Application.Path, 195 | LayerPath: layer.Path, 196 | Manifest: props, 197 | }.Configure(inputArgs) 198 | Expect(err).ToNot(HaveOccurred()) 199 | Expect(startClass).To(Equal("test-start-class")) 200 | Expect(args).To(HaveLen(5)) 201 | Expect(args).To(Equal([]string{ 202 | "stuff", 203 | fmt.Sprintf("-H:Name=%s/test-start-class", layer.Path), 204 | "-cp", 205 | fmt.Sprintf("%s:%s", ctx.Application.Path, "manifest-class-path"), 206 | "test-start-class"})) 207 | }) 208 | 209 | it("fails to find start or main class", func() { 210 | inputArgs := []string{"stuff"} 211 | _, _, err := native.ExplodedJarArguments{ 212 | ApplicationPath: ctx.Application.Path, 213 | LayerPath: layer.Path, 214 | Manifest: properties.NewProperties(), 215 | }.Configure(inputArgs) 216 | Expect(err).To(MatchError("unable to read Start-Class or Main-Class from MANIFEST.MF")) 217 | }) 218 | 219 | context("CLASSPATH is set", func() { 220 | it.Before(func() { 221 | Expect(os.Setenv("CLASSPATH", "some-classpath")).To(Succeed()) 222 | }) 223 | 224 | it.After(func() { 225 | Expect(os.Unsetenv("CLASSPATH")).To(Succeed()) 226 | }) 227 | 228 | it("adds arguments", func() { 229 | inputArgs := []string{"stuff"} 230 | args, startClass, err := native.ExplodedJarArguments{ 231 | ApplicationPath: ctx.Application.Path, 232 | LayerPath: layer.Path, 233 | Manifest: props, 234 | }.Configure(inputArgs) 235 | Expect(err).ToNot(HaveOccurred()) 236 | Expect(startClass).To(Equal("test-start-class")) 237 | Expect(args).To(HaveLen(5)) 238 | Expect(args).To(Equal([]string{ 239 | "stuff", 240 | fmt.Sprintf("-H:Name=%s/test-start-class", layer.Path), 241 | "-cp", 242 | "some-classpath", 243 | "test-start-class"})) 244 | }) 245 | }) 246 | }) 247 | 248 | context("jar file", func() { 249 | it.Before(func() { 250 | Expect(os.MkdirAll(filepath.Join(ctx.Application.Path, "target"), 0755)).To(Succeed()) 251 | Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "target", "found.jar"), []byte{}, 0644)).To(Succeed()) 252 | Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "target", "a.two"), []byte{}, 0644)).To(Succeed()) 253 | Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "target", "b.two"), []byte{}, 0644)).To(Succeed()) 254 | }) 255 | 256 | it("adds arguments", func() { 257 | inputArgs := []string{"stuff"} 258 | args, startClass, err := native.JarArguments{ 259 | ApplicationPath: ctx.Application.Path, 260 | JarFilePattern: "target/*.jar", 261 | }.Configure(inputArgs) 262 | Expect(err).ToNot(HaveOccurred()) 263 | Expect(startClass).To(Equal("found")) 264 | Expect(args).To(HaveLen(3)) 265 | Expect(args).To(Equal([]string{ 266 | "stuff", 267 | "-jar", 268 | filepath.Join(ctx.Application.Path, "target", "found.jar"), 269 | })) 270 | }) 271 | 272 | it("overrides -jar arguments", func() { 273 | inputArgs := []string{"stuff", "-jar", "no-where"} 274 | args, startClass, err := native.JarArguments{ 275 | ApplicationPath: ctx.Application.Path, 276 | JarFilePattern: "target/*.jar", 277 | }.Configure(inputArgs) 278 | Expect(err).ToNot(HaveOccurred()) 279 | Expect(startClass).To(Equal("found")) 280 | Expect(args).To(HaveLen(3)) 281 | Expect(args).To(Equal([]string{ 282 | "stuff", 283 | "-jar", 284 | filepath.Join(ctx.Application.Path, "target", "found.jar"), 285 | })) 286 | }) 287 | 288 | it("pattern doesn't match", func() { 289 | inputArgs := []string{"stuff"} 290 | _, _, err := native.JarArguments{ 291 | ApplicationPath: ctx.Application.Path, 292 | JarFilePattern: "target/*.junk", 293 | }.Configure(inputArgs) 294 | Expect(err).To(MatchError("unable to find single JAR in target/*.junk, candidates: []")) 295 | }) 296 | 297 | it("pattern matches multiple", func() { 298 | inputArgs := []string{"stuff"} 299 | _, _, err := native.JarArguments{ 300 | ApplicationPath: ctx.Application.Path, 301 | JarFilePattern: "target/*.two", 302 | }.Configure(inputArgs) 303 | Expect(err).To(MatchError(MatchRegexp(`unable to find single JAR in target/\*\.two, candidates: \[.*/target/a\.two .*/target/b\.two\]`))) 304 | }) 305 | }) 306 | } 307 | -------------------------------------------------------------------------------- /native/native_image_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package native_test 18 | 19 | import ( 20 | "fmt" 21 | "io" 22 | "os" 23 | "path/filepath" 24 | "strings" 25 | "testing" 26 | 27 | "github.com/buildpacks/libcnb" 28 | "github.com/magiconair/properties" 29 | . "github.com/onsi/gomega" 30 | "github.com/paketo-buildpacks/libpak" 31 | "github.com/paketo-buildpacks/libpak/bard" 32 | "github.com/paketo-buildpacks/libpak/effect" 33 | "github.com/paketo-buildpacks/libpak/effect/mocks" 34 | "github.com/sclevine/spec" 35 | "github.com/stretchr/testify/mock" 36 | 37 | "github.com/paketo-buildpacks/native-image/v5/native" 38 | ) 39 | 40 | func testNativeImage(t *testing.T, context spec.G, it spec.S) { 41 | var ( 42 | Expect = NewWithT(t).Expect 43 | 44 | ctx libcnb.BuildContext 45 | executor *mocks.Executor 46 | props *properties.Properties 47 | nativeImage native.NativeImage 48 | layer libcnb.Layer 49 | ) 50 | 51 | it.Before(func() { 52 | ctx.Application.Path = t.TempDir() 53 | ctx.Layers.Path = t.TempDir() 54 | 55 | executor = &mocks.Executor{} 56 | 57 | props = properties.NewProperties() 58 | 59 | _, _, err := props.Set("Start-Class", "test-start-class") 60 | Expect(err).NotTo(HaveOccurred()) 61 | _, _, err = props.Set("Class-Path", "manifest-class-path") 62 | Expect(err).NotTo(HaveOccurred()) 63 | 64 | Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "fixture-marker"), []byte{}, 0644)).To(Succeed()) 65 | Expect(os.MkdirAll(filepath.Join(ctx.Application.Path, "BOOT-INF"), 0755)).To(Succeed()) 66 | Expect(os.MkdirAll(filepath.Join(ctx.Application.Path, "META-INF"), 0755)).To(Succeed()) 67 | Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "META-INF", "MANIFEST.MF"), []byte{}, 0644)).To(Succeed()) 68 | 69 | nativeImage, err = native.NewNativeImage(ctx.Application.Path, "test-argument-1 test-argument-2", "", "none", "", props, ctx.StackID) 70 | nativeImage.Logger = bard.NewLogger(io.Discard) 71 | Expect(err).NotTo(HaveOccurred()) 72 | nativeImage.Executor = executor 73 | 74 | executor.On("Execute", mock.MatchedBy(func(e effect.Execution) bool { 75 | return e.Command == "native-image" && len(e.Args) == 1 && e.Args[0] == "--version" 76 | })).Run(func(args mock.Arguments) { 77 | exec := args.Get(0).(effect.Execution) 78 | _, err := exec.Stdout.Write([]byte("1.2.3")) 79 | Expect(err).To(Succeed()) 80 | }).Return(nil) 81 | 82 | executor.On("Execute", mock.MatchedBy(func(e effect.Execution) bool { 83 | return e.Command == "native-image" && 84 | (strings.HasPrefix(e.Args[0], "@")) 85 | })).Run(func(args mock.Arguments) { 86 | exec := args.Get(0).(effect.Execution) 87 | lastArg := exec.Args[len(exec.Args)-1] 88 | Expect(os.WriteFile(filepath.Join(layer.Path, lastArg), []byte{}, 0755)).To(Succeed()) 89 | Expect(os.WriteFile(filepath.Join(layer.Path, "libawt.so"), []byte{}, 0644)).To(Succeed()) 90 | Expect(os.WriteFile(filepath.Join(layer.Path, "libawt_headless.so"), []byte{}, 0644)).To(Succeed()) 91 | }).Return(nil) 92 | 93 | executor.On("Execute", mock.MatchedBy(func(e effect.Execution) bool { 94 | return e.Command == "native-image" && 95 | (e.Args[0] == "--no-fallback" || (e.Args[1] == "-H:+StaticExecutableWithDynamicLibC" && e.Args[0] == "--no-fallback")) 96 | })).Run(func(args mock.Arguments) { 97 | exec := args.Get(0).(effect.Execution) 98 | lastArg := exec.Args[len(exec.Args)-1] 99 | Expect(os.WriteFile(filepath.Join(layer.Path, lastArg), []byte{}, 0755)).To(Succeed()) 100 | Expect(os.WriteFile(filepath.Join(layer.Path, "libawt.so"), []byte{}, 0644)).To(Succeed()) 101 | Expect(os.WriteFile(filepath.Join(layer.Path, "libawt_headless.so"), []byte{}, 0644)).To(Succeed()) 102 | }).Return(nil) 103 | 104 | layer, err = ctx.Layers.Layer("test-layer") 105 | Expect(err).NotTo(HaveOccurred()) 106 | }) 107 | 108 | it.After(func() { 109 | Expect(os.RemoveAll(ctx.Application.Path)).To(Succeed()) 110 | Expect(os.RemoveAll(ctx.Layers.Path)).To(Succeed()) 111 | }) 112 | 113 | context("CLASSPATH is set", func() { 114 | it.Before(func() { 115 | Expect(os.Setenv("CLASSPATH", "some-classpath")).To(Succeed()) 116 | }) 117 | 118 | it.After(func() { 119 | Expect(os.Unsetenv("CLASSPATH")).To(Succeed()) 120 | }) 121 | 122 | it("contributes native image", func() { 123 | _, err := nativeImage.Contribute(layer) 124 | Expect(err).NotTo(HaveOccurred()) 125 | 126 | execution := executor.Calls[1].Arguments[0].(effect.Execution) 127 | Expect(execution.Args).To(Equal([]string{ 128 | "--no-fallback", 129 | "test-argument-1", 130 | "test-argument-2", 131 | fmt.Sprintf("-H:Name=%s", filepath.Join(layer.Path, "test-start-class")), 132 | "-cp", "some-classpath", 133 | "test-start-class", 134 | })) 135 | 136 | Expect(filepath.Join(ctx.Application.Path, "BOOT-INF")).ToNot(BeADirectory()) 137 | Expect(filepath.Join(ctx.Application.Path, "META-INF")).ToNot(BeADirectory()) 138 | 139 | Expect(filepath.Join(layer.Path, "test-start-class")).To(BeARegularFile()) 140 | Expect(filepath.Join(layer.Path, "libawt.so")).To(BeARegularFile()) 141 | Expect(filepath.Join(layer.Path, "libawt_headless.so")).To(BeARegularFile()) 142 | 143 | info, err := os.Stat(filepath.Join(layer.Path, "test-start-class")) 144 | Expect(err).NotTo(HaveOccurred()) 145 | fmt.Println("info.Mode().Perm(): ", info.Mode().Perm().String()) 146 | Expect(info.Mode().Perm()).To(Equal(os.FileMode(0755))) 147 | 148 | Expect(filepath.Join(ctx.Application.Path, "test-start-class")).To(BeARegularFile()) 149 | Expect(filepath.Join(ctx.Application.Path, "libawt.so")).To(BeARegularFile()) 150 | Expect(filepath.Join(ctx.Application.Path, "libawt_headless.so")).To(BeARegularFile()) 151 | info, err = os.Stat(filepath.Join(ctx.Application.Path, "test-start-class")) 152 | Expect(err).NotTo(HaveOccurred()) 153 | Expect(info.Mode().Perm()).To(Equal(os.FileMode(0755))) 154 | }) 155 | }) 156 | 157 | context("CLASSPATH is not set", func() { 158 | it("contributes native image with Class-Path from manifest", func() { 159 | _, err := nativeImage.Contribute(layer) 160 | Expect(err).NotTo(HaveOccurred()) 161 | 162 | execution := executor.Calls[1].Arguments[0].(effect.Execution) 163 | Expect(execution.Args).To(Equal([]string{ 164 | "--no-fallback", 165 | "test-argument-1", 166 | "test-argument-2", 167 | fmt.Sprintf("-H:Name=%s", filepath.Join(layer.Path, "test-start-class")), 168 | "-cp", 169 | strings.Join([]string{ 170 | ctx.Application.Path, 171 | "manifest-class-path", 172 | }, ":"), 173 | "test-start-class", 174 | })) 175 | }) 176 | 177 | it("contributes native image with Class-Path from manifest and args from a file", func() { 178 | argsFile := filepath.Join(ctx.Application.Path, "target", "args.txt") 179 | Expect(os.MkdirAll(filepath.Join(ctx.Application.Path, "target"), 0755)).To(Succeed()) 180 | Expect(os.WriteFile(argsFile, []byte(`test-argument-1 test-argument-2`), 0644)).To(Succeed()) 181 | 182 | nativeImage, err := native.NewNativeImage(ctx.Application.Path, "", argsFile, "none", "", props, ctx.StackID) 183 | nativeImage.Logger = bard.NewLogger(io.Discard) 184 | Expect(err).NotTo(HaveOccurred()) 185 | nativeImage.Executor = executor 186 | 187 | _, err = nativeImage.Contribute(layer) 188 | Expect(err).NotTo(HaveOccurred()) 189 | 190 | execution := executor.Calls[1].Arguments[0].(effect.Execution) 191 | Expect(execution.Args).To(Equal([]string{ 192 | "--no-fallback", 193 | fmt.Sprintf("@%s", argsFile), 194 | fmt.Sprintf("-H:Name=%s", filepath.Join(layer.Path, "test-start-class")), 195 | "-cp", 196 | strings.Join([]string{ 197 | ctx.Application.Path, 198 | "manifest-class-path", 199 | }, ":"), 200 | "test-start-class", 201 | })) 202 | }) 203 | }) 204 | 205 | context("user opts out of --no-fallback", func() { 206 | var err error 207 | 208 | it("contributes native image with --force-fallback", func() { 209 | executorForceFallback := &mocks.Executor{} 210 | nativeImage, err = native.NewNativeImage(ctx.Application.Path, "--force-fallback test-argument-1 test-argument-2", "", "none", "", props, ctx.StackID) 211 | nativeImage.Logger = bard.NewLogger(io.Discard) 212 | Expect(err).NotTo(HaveOccurred()) 213 | nativeImage.Executor = executorForceFallback 214 | 215 | executorForceFallback.On("Execute", mock.MatchedBy(func(e effect.Execution) bool { 216 | return e.Command == "native-image" && len(e.Args) == 1 && e.Args[0] == "--version" 217 | })).Run(func(args mock.Arguments) { 218 | exec := args.Get(0).(effect.Execution) 219 | _, err := exec.Stdout.Write([]byte("1.2.3")) 220 | Expect(err).To(Succeed()) 221 | }).Return(nil) 222 | 223 | executorForceFallback.On("Execute", mock.MatchedBy(func(e effect.Execution) bool { 224 | return e.Command == "native-image" && 225 | (e.Args[0] == "--force-fallback" || (e.Args[1] == "-H:+StaticExecutableWithDynamicLibC" && e.Args[0] == "--force-fallback")) 226 | })).Run(func(args mock.Arguments) { 227 | exec := args.Get(0).(effect.Execution) 228 | lastArg := exec.Args[len(exec.Args)-1] 229 | Expect(os.WriteFile(filepath.Join(layer.Path, lastArg), []byte{}, 0644)).To(Succeed()) 230 | }).Return(nil) 231 | 232 | layer, err = ctx.Layers.Layer("test-layer") 233 | Expect(err).NotTo(HaveOccurred()) 234 | 235 | _, err := nativeImage.Contribute(layer) 236 | Expect(err).NotTo(HaveOccurred()) 237 | 238 | execution := executorForceFallback.Calls[1].Arguments[0].(effect.Execution) 239 | Expect(execution.Args).To(Equal([]string{ 240 | "--force-fallback", 241 | "test-argument-1", 242 | "test-argument-2", 243 | fmt.Sprintf("-H:Name=%s", filepath.Join(layer.Path, "test-start-class")), 244 | "-cp", 245 | strings.Join([]string{ 246 | ctx.Application.Path, 247 | "manifest-class-path", 248 | }, ":"), 249 | "test-start-class", 250 | })) 251 | }) 252 | 253 | it("contributes native image with --auto-fallback", func() { 254 | executorAutoFallback := &mocks.Executor{} 255 | nativeImage, err = native.NewNativeImage(ctx.Application.Path, "--auto-fallback test-argument-1 test-argument-2", "", "none", "", props, ctx.StackID) 256 | nativeImage.Logger = bard.NewLogger(io.Discard) 257 | Expect(err).NotTo(HaveOccurred()) 258 | nativeImage.Executor = executorAutoFallback 259 | 260 | executorAutoFallback.On("Execute", mock.MatchedBy(func(e effect.Execution) bool { 261 | return e.Command == "native-image" && len(e.Args) == 1 && e.Args[0] == "--version" 262 | })).Run(func(args mock.Arguments) { 263 | exec := args.Get(0).(effect.Execution) 264 | _, err := exec.Stdout.Write([]byte("1.2.3")) 265 | Expect(err).To(Succeed()) 266 | }).Return(nil) 267 | 268 | executorAutoFallback.On("Execute", mock.MatchedBy(func(e effect.Execution) bool { 269 | return e.Command == "native-image" && 270 | (e.Args[0] == "--auto-fallback" || (e.Args[1] == "-H:+StaticExecutableWithDynamicLibC" && e.Args[0] == "--auto-fallback")) 271 | })).Run(func(args mock.Arguments) { 272 | exec := args.Get(0).(effect.Execution) 273 | lastArg := exec.Args[len(exec.Args)-1] 274 | Expect(os.WriteFile(filepath.Join(layer.Path, lastArg), []byte{}, 0644)).To(Succeed()) 275 | }).Return(nil) 276 | 277 | layer, err = ctx.Layers.Layer("test-layer") 278 | Expect(err).NotTo(HaveOccurred()) 279 | 280 | _, err := nativeImage.Contribute(layer) 281 | Expect(err).NotTo(HaveOccurred()) 282 | 283 | execution := executorAutoFallback.Calls[1].Arguments[0].(effect.Execution) 284 | Expect(execution.Args).To(Equal([]string{ 285 | "--auto-fallback", 286 | "test-argument-1", 287 | "test-argument-2", 288 | fmt.Sprintf("-H:Name=%s", filepath.Join(layer.Path, "test-start-class")), 289 | "-cp", 290 | strings.Join([]string{ 291 | ctx.Application.Path, 292 | "manifest-class-path", 293 | }, ":"), 294 | "test-start-class", 295 | })) 296 | }) 297 | }) 298 | 299 | context("Not a Spring Boot app", func() { 300 | it.Before(func() { 301 | // there won't be a Start-Class 302 | props.Delete("Start-Class") 303 | 304 | // we do expect a Main-Class 305 | _, _, err := props.Set("Main-Class", "test-main-class") 306 | Expect(err).NotTo(HaveOccurred()) 307 | }) 308 | 309 | it("contributes native image using Main-Class", func() { 310 | _, err := nativeImage.Contribute(layer) 311 | Expect(err).NotTo(HaveOccurred()) 312 | 313 | execution := executor.Calls[1].Arguments[0].(effect.Execution) 314 | Expect(execution.Args).To(Equal([]string{ 315 | "--no-fallback", 316 | "test-argument-1", 317 | "test-argument-2", 318 | fmt.Sprintf("-H:Name=%s", filepath.Join(layer.Path, "test-main-class")), 319 | "-cp", 320 | strings.Join([]string{ 321 | ctx.Application.Path, 322 | "manifest-class-path", 323 | }, ":"), 324 | "test-main-class", 325 | })) 326 | }) 327 | }) 328 | 329 | context("upx compression is used", func() { 330 | it("contributes native image and runs compression", func() { 331 | nativeImage.Compressor = "upx" 332 | 333 | executor.On("Execute", mock.MatchedBy(func(e effect.Execution) bool { 334 | return e.Command == "upx" 335 | })).Run(func(args mock.Arguments) { 336 | Expect(os.WriteFile(filepath.Join(layer.Path, "test-start-class"), []byte("upx-compressed"), 0644)).To(Succeed()) 337 | }).Return(nil) 338 | 339 | _, err := nativeImage.Contribute(layer) 340 | Expect(err).NotTo(HaveOccurred()) 341 | 342 | execution := executor.Calls[1].Arguments[0].(effect.Execution) 343 | Expect(execution.Command).To(Equal("native-image")) 344 | 345 | execution = executor.Calls[2].Arguments[0].(effect.Execution) 346 | Expect(execution.Command).To(Equal("upx")) 347 | 348 | bin := filepath.Join(layer.Path, "test-start-class") 349 | Expect(bin).To(BeARegularFile()) 350 | 351 | data, err := os.ReadFile(bin) 352 | Expect(err).ToNot(HaveOccurred()) 353 | Expect(data).To(ContainSubstring("upx-compressed")) 354 | }) 355 | }) 356 | 357 | context("gzexe compression is used", func() { 358 | it("contributes native image and runs compression", func() { 359 | nativeImage.Compressor = "gzexe" 360 | 361 | executor.On("Execute", mock.MatchedBy(func(e effect.Execution) bool { 362 | return e.Command == "gzexe" 363 | })).Run(func(args mock.Arguments) { 364 | Expect(os.WriteFile(filepath.Join(layer.Path, "test-start-class"), []byte("gzexe-compressed"), 0644)).To(Succeed()) 365 | Expect(os.WriteFile(filepath.Join(layer.Path, "test-start-class~"), []byte("original"), 0644)).To(Succeed()) 366 | }).Return(nil) 367 | 368 | _, err := nativeImage.Contribute(layer) 369 | Expect(err).NotTo(HaveOccurred()) 370 | 371 | execution := executor.Calls[1].Arguments[0].(effect.Execution) 372 | Expect(execution.Command).To(Equal("native-image")) 373 | 374 | execution = executor.Calls[2].Arguments[0].(effect.Execution) 375 | Expect(execution.Command).To(Equal("gzexe")) 376 | 377 | bin := filepath.Join(layer.Path, "test-start-class") 378 | Expect(bin).To(BeARegularFile()) 379 | 380 | data, err := os.ReadFile(bin) 381 | Expect(err).ToNot(HaveOccurred()) 382 | Expect(data).To(ContainSubstring("gzexe-compressed")) 383 | Expect(filepath.Join(layer.Path, "test-start-class~")).ToNot(BeAnExistingFile()) 384 | }) 385 | }) 386 | 387 | context("tiny stack", func() { 388 | it.Before(func() { 389 | nativeImage.StackID = libpak.TinyStackID 390 | }) 391 | 392 | it("contributes a static native image executable with dynamic libc", func() { 393 | Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "BOOT-INF", "classpath.idx"), []byte(` 394 | - "test-jar.jar" 395 | - "spring-graalvm-native-0.8.6-xxxxxx.jar" 396 | `), 0644)).To(Succeed()) 397 | var err error 398 | layer, err := nativeImage.Contribute(layer) 399 | Expect(err).NotTo(HaveOccurred()) 400 | 401 | Expect(layer.Cache).To(BeTrue()) 402 | Expect(filepath.Join(layer.Path, "test-start-class")).To(BeARegularFile()) 403 | Expect(filepath.Join(ctx.Application.Path, "test-start-class")).To(BeARegularFile()) 404 | Expect(filepath.Join(ctx.Application.Path, "fixture-marker")).NotTo(BeAnExistingFile()) 405 | 406 | execution := executor.Calls[1].Arguments[0].(effect.Execution) 407 | Expect(execution.Command).To(Equal("native-image")) 408 | Expect(execution.Args).To(Equal([]string{ 409 | "--no-fallback", 410 | "-H:+StaticExecutableWithDynamicLibC", 411 | "test-argument-1", 412 | "test-argument-2", 413 | fmt.Sprintf("-H:Name=%s", filepath.Join(layer.Path, "test-start-class")), 414 | "-cp", 415 | strings.Join([]string{ 416 | ctx.Application.Path, 417 | "manifest-class-path", 418 | }, ":"), 419 | "test-start-class", 420 | })) 421 | Expect(execution.Dir).To(Equal(layer.Path)) 422 | }) 423 | }) 424 | } 425 | -------------------------------------------------------------------------------- /native/detect_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package native_test 18 | 19 | import ( 20 | "os" 21 | "testing" 22 | 23 | "github.com/buildpacks/libcnb" 24 | . "github.com/onsi/gomega" 25 | "github.com/sclevine/spec" 26 | 27 | "github.com/paketo-buildpacks/native-image/v5/native" 28 | ) 29 | 30 | func testDetect(t *testing.T, context spec.G, it spec.S) { 31 | var ( 32 | Expect = NewWithT(t).Expect 33 | 34 | ctx libcnb.DetectContext 35 | detect native.Detect 36 | ) 37 | 38 | context("neither BP_NATIVE_IMAGE nor BP_BOOT_NATIVE_IMAGE are set", func() { 39 | it("provides but does not requires native-image-application", func() { 40 | Expect(detect.Detect(ctx)).To(Equal(libcnb.DetectResult{ 41 | Pass: true, 42 | Plans: []libcnb.BuildPlan{ 43 | { 44 | Provides: []libcnb.BuildPlanProvide{ 45 | {Name: "native-image-application"}, 46 | }, 47 | Requires: []libcnb.BuildPlanRequire{ 48 | { 49 | Name: "native-image-builder", 50 | }, 51 | { 52 | Name: "jvm-application", 53 | Metadata: map[string]interface{}{"native-image": true}, 54 | }, 55 | { 56 | Name: "spring-boot", 57 | Metadata: map[string]interface{}{"native-image": true}, 58 | }, 59 | }, 60 | }, 61 | { 62 | Provides: []libcnb.BuildPlanProvide{ 63 | {Name: "native-image-application"}, 64 | }, 65 | Requires: []libcnb.BuildPlanRequire{ 66 | { 67 | Name: "native-image-builder", 68 | }, 69 | { 70 | Name: "native-processed", 71 | }, 72 | { 73 | Name: "native-image-application", 74 | }, 75 | }, 76 | }, 77 | { 78 | Provides: []libcnb.BuildPlanProvide{ 79 | {Name: "native-image-application"}, 80 | }, 81 | Requires: []libcnb.BuildPlanRequire{ 82 | { 83 | Name: "native-image-builder", 84 | }, 85 | { 86 | Name: "jvm-application", 87 | Metadata: map[string]interface{}{"native-image": true}, 88 | }, 89 | }, 90 | }, 91 | }, 92 | })) 93 | }) 94 | }) 95 | 96 | context("$BP_NATIVE_IMAGE", func() { 97 | context("true", func() { 98 | it.Before(func() { 99 | Expect(os.Setenv("BP_NATIVE_IMAGE", "true")).To(Succeed()) 100 | }) 101 | 102 | it.After(func() { 103 | Expect(os.Unsetenv("BP_NATIVE_IMAGE")).To(Succeed()) 104 | }) 105 | 106 | it("provides and requires native-image-application", func() { 107 | Expect(detect.Detect(ctx)).To(Equal(libcnb.DetectResult{ 108 | Pass: true, 109 | Plans: []libcnb.BuildPlan{ 110 | { 111 | Provides: []libcnb.BuildPlanProvide{ 112 | {Name: "native-image-application"}, 113 | }, 114 | Requires: []libcnb.BuildPlanRequire{ 115 | { 116 | Name: "native-image-builder", 117 | }, 118 | { 119 | Name: "jvm-application", 120 | Metadata: map[string]interface{}{"native-image": true}, 121 | }, 122 | { 123 | Name: "spring-boot", 124 | Metadata: map[string]interface{}{"native-image": true}, 125 | }, 126 | { 127 | Name: "native-image-application", 128 | }, 129 | }, 130 | }, 131 | { 132 | Provides: []libcnb.BuildPlanProvide{ 133 | {Name: "native-image-application"}, 134 | }, 135 | Requires: []libcnb.BuildPlanRequire{ 136 | { 137 | Name: "native-image-builder", 138 | }, 139 | { 140 | Name: "native-processed", 141 | }, 142 | { 143 | Name: "native-image-application", 144 | }, 145 | }, 146 | }, 147 | { 148 | Provides: []libcnb.BuildPlanProvide{ 149 | {Name: "native-image-application"}, 150 | }, 151 | Requires: []libcnb.BuildPlanRequire{ 152 | { 153 | Name: "native-image-builder", 154 | }, 155 | { 156 | Name: "jvm-application", 157 | Metadata: map[string]interface{}{"native-image": true}, 158 | }, 159 | { 160 | Name: "native-image-application", 161 | }, 162 | }, 163 | }, 164 | }, 165 | })) 166 | }) 167 | }) 168 | 169 | context("false", func() { 170 | it.Before(func() { 171 | Expect(os.Setenv("BP_NATIVE_IMAGE", "false")).To(Succeed()) 172 | }) 173 | 174 | it.After(func() { 175 | Expect(os.Unsetenv("BP_NATIVE_IMAGE")).To(Succeed()) 176 | }) 177 | 178 | it("provides but does not requires native-image-application", func() { 179 | Expect(detect.Detect(ctx)).To(Equal(libcnb.DetectResult{ 180 | Pass: true, 181 | Plans: []libcnb.BuildPlan{ 182 | { 183 | Provides: []libcnb.BuildPlanProvide{ 184 | {Name: "native-image-application"}, 185 | }, 186 | Requires: []libcnb.BuildPlanRequire{ 187 | { 188 | Name: "native-image-builder", 189 | }, 190 | { 191 | Name: "jvm-application", 192 | Metadata: map[string]interface{}{"native-image": true}, 193 | }, 194 | { 195 | Name: "spring-boot", 196 | Metadata: map[string]interface{}{"native-image": true}, 197 | }, 198 | }, 199 | }, 200 | { 201 | Provides: []libcnb.BuildPlanProvide{ 202 | {Name: "native-image-application"}, 203 | }, 204 | Requires: []libcnb.BuildPlanRequire{ 205 | { 206 | Name: "native-image-builder", 207 | }, 208 | { 209 | Name: "native-processed", 210 | }, 211 | { 212 | Name: "native-image-application", 213 | }, 214 | }, 215 | }, 216 | { 217 | Provides: []libcnb.BuildPlanProvide{ 218 | {Name: "native-image-application"}, 219 | }, 220 | Requires: []libcnb.BuildPlanRequire{ 221 | { 222 | Name: "native-image-builder", 223 | }, 224 | { 225 | Name: "jvm-application", 226 | Metadata: map[string]interface{}{"native-image": true}, 227 | }, 228 | }, 229 | }, 230 | }, 231 | })) 232 | }) 233 | }) 234 | 235 | context("not a bool", func() { 236 | it.Before(func() { 237 | Expect(os.Setenv("BP_NATIVE_IMAGE", "foo")).To(Succeed()) 238 | }) 239 | 240 | it.After(func() { 241 | Expect(os.Unsetenv("BP_NATIVE_IMAGE")).To(Succeed()) 242 | }) 243 | 244 | it("errors", func() { 245 | _, err := detect.Detect(ctx) 246 | Expect(err).To(HaveOccurred()) 247 | }) 248 | }) 249 | }) 250 | 251 | context("$BP_BINARY_COMPRESSION_METHOD", func() { 252 | it.Before(func() { 253 | Expect(os.Setenv("BP_NATIVE_IMAGE", "true")).To(Succeed()) 254 | }) 255 | 256 | it.After(func() { 257 | Expect(os.Unsetenv("BP_NATIVE_IMAGE")).To(Succeed()) 258 | }) 259 | 260 | context("upx", func() { 261 | it.Before(func() { 262 | Expect(os.Setenv("BP_BINARY_COMPRESSION_METHOD", "upx")).To(Succeed()) 263 | }) 264 | 265 | it.After(func() { 266 | Expect(os.Unsetenv("BP_BINARY_COMPRESSION_METHOD")).To(Succeed()) 267 | }) 268 | 269 | it("requires upx", func() { 270 | Expect(detect.Detect(ctx)).To(Equal(libcnb.DetectResult{ 271 | Pass: true, 272 | Plans: []libcnb.BuildPlan{ 273 | { 274 | Provides: []libcnb.BuildPlanProvide{ 275 | {Name: "native-image-application"}, 276 | }, 277 | Requires: []libcnb.BuildPlanRequire{ 278 | { 279 | Name: "native-image-builder", 280 | }, 281 | { 282 | Name: "jvm-application", 283 | Metadata: map[string]interface{}{"native-image": true}, 284 | }, 285 | { 286 | Name: "spring-boot", 287 | Metadata: map[string]interface{}{"native-image": true}, 288 | }, 289 | { 290 | Name: "native-image-application", 291 | }, 292 | { 293 | Name: "upx", 294 | }, 295 | }, 296 | }, 297 | { 298 | Provides: []libcnb.BuildPlanProvide{ 299 | {Name: "native-image-application"}, 300 | }, 301 | Requires: []libcnb.BuildPlanRequire{ 302 | { 303 | Name: "native-image-builder", 304 | }, 305 | { 306 | Name: "native-processed", 307 | }, 308 | { 309 | Name: "native-image-application", 310 | }, 311 | { 312 | Name: "upx", 313 | }, 314 | }, 315 | }, 316 | { 317 | Provides: []libcnb.BuildPlanProvide{ 318 | {Name: "native-image-application"}, 319 | }, 320 | Requires: []libcnb.BuildPlanRequire{ 321 | { 322 | Name: "native-image-builder", 323 | }, 324 | { 325 | Name: "jvm-application", 326 | Metadata: map[string]interface{}{"native-image": true}, 327 | }, 328 | { 329 | Name: "native-image-application", 330 | }, 331 | { 332 | Name: "upx", 333 | }, 334 | }, 335 | }, 336 | }, 337 | })) 338 | }) 339 | }) 340 | 341 | context("gzexe", func() { 342 | it.Before(func() { 343 | Expect(os.Setenv("BP_BINARY_COMPRESSION_METHOD", "gzexe")).To(Succeed()) 344 | }) 345 | 346 | it.After(func() { 347 | Expect(os.Unsetenv("BP_BINARY_COMPRESSION_METHOD")).To(Succeed()) 348 | }) 349 | 350 | it("no additional provides or requires", func() { 351 | Expect(detect.Detect(ctx)).To(Equal(libcnb.DetectResult{ 352 | Pass: true, 353 | Plans: []libcnb.BuildPlan{ 354 | { 355 | Provides: []libcnb.BuildPlanProvide{ 356 | {Name: "native-image-application"}, 357 | }, 358 | Requires: []libcnb.BuildPlanRequire{ 359 | { 360 | Name: "native-image-builder", 361 | }, 362 | { 363 | Name: "jvm-application", 364 | Metadata: map[string]interface{}{"native-image": true}, 365 | }, 366 | { 367 | Name: "spring-boot", 368 | Metadata: map[string]interface{}{"native-image": true}, 369 | }, 370 | { 371 | Name: "native-image-application", 372 | }, 373 | }, 374 | }, 375 | { 376 | Provides: []libcnb.BuildPlanProvide{ 377 | {Name: "native-image-application"}, 378 | }, 379 | Requires: []libcnb.BuildPlanRequire{ 380 | { 381 | Name: "native-image-builder", 382 | }, 383 | { 384 | Name: "native-processed", 385 | }, 386 | { 387 | Name: "native-image-application", 388 | }, 389 | }, 390 | }, 391 | { 392 | Provides: []libcnb.BuildPlanProvide{ 393 | {Name: "native-image-application"}, 394 | }, 395 | Requires: []libcnb.BuildPlanRequire{ 396 | { 397 | Name: "native-image-builder", 398 | }, 399 | { 400 | Name: "jvm-application", 401 | Metadata: map[string]interface{}{"native-image": true}, 402 | }, 403 | { 404 | Name: "native-image-application", 405 | }, 406 | }, 407 | }, 408 | }, 409 | })) 410 | }) 411 | }) 412 | 413 | context("none", func() { 414 | it.Before(func() { 415 | Expect(os.Setenv("BP_BINARY_COMPRESSION_METHOD", "none")).To(Succeed()) 416 | }) 417 | 418 | it.After(func() { 419 | Expect(os.Unsetenv("BP_BINARY_COMPRESSION_METHOD")).To(Succeed()) 420 | }) 421 | 422 | it("no additional provides or requires", func() { 423 | Expect(detect.Detect(ctx)).To(Equal(libcnb.DetectResult{ 424 | Pass: true, 425 | Plans: []libcnb.BuildPlan{ 426 | { 427 | Provides: []libcnb.BuildPlanProvide{ 428 | {Name: "native-image-application"}, 429 | }, 430 | Requires: []libcnb.BuildPlanRequire{ 431 | { 432 | Name: "native-image-builder", 433 | }, 434 | { 435 | Name: "jvm-application", 436 | Metadata: map[string]interface{}{"native-image": true}, 437 | }, 438 | { 439 | Name: "spring-boot", 440 | Metadata: map[string]interface{}{"native-image": true}, 441 | }, 442 | { 443 | Name: "native-image-application", 444 | }, 445 | }, 446 | }, 447 | { 448 | Provides: []libcnb.BuildPlanProvide{ 449 | {Name: "native-image-application"}, 450 | }, 451 | Requires: []libcnb.BuildPlanRequire{ 452 | { 453 | Name: "native-image-builder", 454 | }, 455 | { 456 | Name: "native-processed", 457 | }, 458 | { 459 | Name: "native-image-application", 460 | }, 461 | }, 462 | }, 463 | { 464 | Provides: []libcnb.BuildPlanProvide{ 465 | {Name: "native-image-application"}, 466 | }, 467 | Requires: []libcnb.BuildPlanRequire{ 468 | { 469 | Name: "native-image-builder", 470 | }, 471 | { 472 | Name: "jvm-application", 473 | Metadata: map[string]interface{}{"native-image": true}, 474 | }, 475 | { 476 | Name: "native-image-application", 477 | }, 478 | }, 479 | }, 480 | }, 481 | })) 482 | }) 483 | }) 484 | 485 | context("not a supported method", func() { 486 | it.Before(func() { 487 | Expect(os.Setenv("BP_BINARY_COMPRESSION_METHOD", "foo")).To(Succeed()) 488 | }) 489 | 490 | it.After(func() { 491 | Expect(os.Unsetenv("BP_BINARY_COMPRESSION_METHOD")).To(Succeed()) 492 | }) 493 | 494 | it("ignore and no additional provides or requires", func() { 495 | Expect(detect.Detect(ctx)).To(Equal(libcnb.DetectResult{ 496 | Pass: true, 497 | Plans: []libcnb.BuildPlan{ 498 | { 499 | Provides: []libcnb.BuildPlanProvide{ 500 | {Name: "native-image-application"}, 501 | }, 502 | Requires: []libcnb.BuildPlanRequire{ 503 | { 504 | Name: "native-image-builder", 505 | }, 506 | { 507 | Name: "jvm-application", 508 | Metadata: map[string]interface{}{"native-image": true}, 509 | }, 510 | { 511 | Name: "spring-boot", 512 | Metadata: map[string]interface{}{"native-image": true}, 513 | }, 514 | { 515 | Name: "native-image-application", 516 | }, 517 | }, 518 | }, 519 | { 520 | Provides: []libcnb.BuildPlanProvide{ 521 | {Name: "native-image-application"}, 522 | }, 523 | Requires: []libcnb.BuildPlanRequire{ 524 | { 525 | Name: "native-image-builder", 526 | }, 527 | { 528 | Name: "native-processed", 529 | }, 530 | { 531 | Name: "native-image-application", 532 | }, 533 | }, 534 | }, 535 | { 536 | Provides: []libcnb.BuildPlanProvide{ 537 | {Name: "native-image-application"}, 538 | }, 539 | Requires: []libcnb.BuildPlanRequire{ 540 | { 541 | Name: "native-image-builder", 542 | }, 543 | { 544 | Name: "jvm-application", 545 | Metadata: map[string]interface{}{"native-image": true}, 546 | }, 547 | { 548 | Name: "native-image-application", 549 | }, 550 | }, 551 | }, 552 | }, 553 | })) 554 | }) 555 | }) 556 | }) 557 | 558 | context("$BP_BOOT_NATIVE_IMAGE", func() { 559 | it.Before(func() { 560 | Expect(os.Setenv("BP_BOOT_NATIVE_IMAGE", "true")).To(Succeed()) 561 | }) 562 | 563 | it.After(func() { 564 | Expect(os.Unsetenv("BP_BOOT_NATIVE_IMAGE")).To(Succeed()) 565 | }) 566 | 567 | it("provides and requires native-image-application", func() { 568 | Expect(detect.Detect(ctx)).To(Equal(libcnb.DetectResult{ 569 | Pass: true, 570 | Plans: []libcnb.BuildPlan{ 571 | { 572 | Provides: []libcnb.BuildPlanProvide{ 573 | {Name: "native-image-application"}, 574 | }, 575 | Requires: []libcnb.BuildPlanRequire{ 576 | { 577 | Name: "native-image-builder", 578 | }, 579 | { 580 | Name: "jvm-application", 581 | Metadata: map[string]interface{}{"native-image": true}, 582 | }, 583 | { 584 | Name: "spring-boot", 585 | Metadata: map[string]interface{}{"native-image": true}, 586 | }, 587 | { 588 | Name: "native-image-application", 589 | }, 590 | }, 591 | }, 592 | { 593 | Provides: []libcnb.BuildPlanProvide{ 594 | {Name: "native-image-application"}, 595 | }, 596 | Requires: []libcnb.BuildPlanRequire{ 597 | { 598 | Name: "native-image-builder", 599 | }, 600 | { 601 | Name: "native-processed", 602 | }, 603 | { 604 | Name: "native-image-application", 605 | }, 606 | }, 607 | }, 608 | { 609 | Provides: []libcnb.BuildPlanProvide{ 610 | {Name: "native-image-application"}, 611 | }, 612 | Requires: []libcnb.BuildPlanRequire{ 613 | { 614 | Name: "native-image-builder", 615 | }, 616 | { 617 | Name: "jvm-application", 618 | Metadata: map[string]interface{}{"native-image": true}, 619 | }, 620 | { 621 | Name: "native-image-application", 622 | }, 623 | }, 624 | }, 625 | }, 626 | })) 627 | }) 628 | }) 629 | } 630 | --------------------------------------------------------------------------------