├── .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 |
--------------------------------------------------------------------------------