cmd/java/maven/main.go (188 lines of code) (raw):
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Implements java/maven buildpack.
// The maven buildpack builds Maven applications.
package main
import (
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/GoogleCloudPlatform/buildpacks/pkg/devmode"
"github.com/GoogleCloudPlatform/buildpacks/pkg/env"
"github.com/GoogleCloudPlatform/buildpacks/pkg/fileutil"
gcp "github.com/GoogleCloudPlatform/buildpacks/pkg/gcpbuildpack"
"github.com/GoogleCloudPlatform/buildpacks/pkg/java"
)
const (
// TODO(b/151198698): Automate Maven version updates.
mavenVersion = "3.9.9"
mavenURL = "https://archive.apache.org/dist/maven/maven-3/%[1]s/binaries/apache-maven-%[1]s-bin.tar.gz"
mavenLayer = "maven"
m2Layer = "m2"
versionKey = "version"
)
func main() {
gcp.Main(detectFn, buildFn)
}
func detectFn(ctx *gcp.Context) (gcp.DetectResult, error) {
pomPath, err := pomFilePath(ctx)
if err != nil {
return nil, err
}
if pomPath != "" {
return gcp.OptInFileFound("pom.xml"), nil
}
extXMLExists, err := ctx.FileExists(".mvn/extensions.xml")
if err != nil {
return nil, err
}
if extXMLExists {
return gcp.OptInFileFound(".mvn/extensions.xml"), nil
}
return gcp.OptOut("none of the following found: pom.xml or .mvn/extensions.xml."), nil
}
func buildFn(ctx *gcp.Context) error {
m2CachedRepo, err := ctx.Layer(m2Layer, gcp.CacheLayer, gcp.LaunchLayerIfDevMode)
if err != nil {
return fmt.Errorf("creating %v layer: %w", m2Layer, err)
}
if err := java.CheckCacheExpiration(ctx, m2CachedRepo); err != nil {
return fmt.Errorf("validating the cache: %w", err)
}
homeM2 := filepath.Join(ctx.HomeDir(), ".m2")
// Symlink the m2 layer into ~/.m2. If ~/.m2 already exists, delete it first.
// If it exists as a symlink, RemoveAll will remove the link, not anything it's linked to.
// We can't just use `-Dmaven.repo.local`. It does set the path to `m2/repo` but it fails
// to set the path to `m2/wrapper` which is used by mvnw to download Maven.
if err := ctx.RemoveAll(homeM2); err != nil {
return err
}
if err := ctx.Symlink(m2CachedRepo.Path, homeM2); err != nil {
return err
}
if err := addJvmConfig(ctx); err != nil {
return err
}
mvn, err := provisionOrDetectMaven(ctx)
if err != nil {
return err
}
command := []string{mvn, "clean", "package", "--batch-mode", "-DskipTests", "-Dhttp.keepAlive=false"}
pomPath, err := pomFilePath(ctx)
if err != nil {
return err
}
if pomPath != "" {
command = append(command, fmt.Sprintf("-f=%s", pomPath))
}
if buildArgs := os.Getenv(env.BuildArgs); buildArgs != "" {
if strings.Contains(buildArgs, "maven.repo.local") {
ctx.Warnf("Detected maven.repo.local property set in GOOGLE_BUILD_ARGS. Maven caching may not work properly.")
}
command = append(command, strings.Fields(buildArgs)...)
}
if mvnBuildArgs := os.Getenv(java.MavenBuildArgs); mvnBuildArgs != "" {
command = append([]string{mvn}, strings.Fields(mvnBuildArgs)...)
}
if !ctx.Debug() && !devmode.Enabled(ctx) {
command = append(command, "--quiet")
}
if _, err := ctx.Exec(command, gcp.WithStdoutTail, gcp.WithUserAttribution); err != nil {
return err
}
// Store the build steps in a script to be run on each file change.
if devmode.Enabled(ctx) {
devmode.WriteBuildScript(ctx, m2CachedRepo.Path, "~/.m2", command)
}
return nil
}
func provisionOrDetectMaven(ctx *gcp.Context) (string, error) {
mvnwExists, err := ctx.FileExists("mvnw")
if err != nil {
return "", err
}
if mvnwExists {
// With CRLF endings, the "\r" gets seen as part of the shebang target, which doesn't exist.
if err := fileutil.EnsureUnixLineEndings("mvnw"); err != nil {
return "", fmt.Errorf("ensuring unix newline characters: %w", err)
}
return "./mvnw", nil
}
mvnInstalled, err := mvnInstalled(ctx)
if err != nil {
return "", err
}
if mvnInstalled {
return "mvn", nil
}
mvn, err := installMaven(ctx)
if err != nil {
return "", fmt.Errorf("installing Maven: %w", err)
}
return mvn, nil
}
// addJvmConfig is a workaround for https://github.com/google/guice/issues/1133, an "illegal reflective access" warning.
// When that bug has been fixed in a version of mvn we can use, we can remove this workaround.
// Write a JVM flag to .mvn/jvm.config in the project being built to suppress the warning.
// Don't do anything if there already is a .mvn/jvm.config.
func addJvmConfig(ctx *gcp.Context) error {
version := os.Getenv(env.RuntimeVersion)
if version == "8" || strings.HasPrefix(version, "8.") {
// We don't need this workaround on Java 8, and in fact it fails there because there's no --add-opens option.
return nil
}
configFile := ".mvn/jvm.config"
configFileExists, err := ctx.FileExists(configFile)
if err != nil {
return err
}
if configFileExists {
return nil
}
if err := os.MkdirAll(".mvn", 0755); err != nil {
ctx.Logf("Could not create .mvn, reflection warnings may not be disabled: %v", err)
return nil
}
jvmOptions := "--add-opens java.base/java.lang=ALL-UNNAMED"
if err := ioutil.WriteFile(configFile, []byte(jvmOptions), 0644); err != nil {
ctx.Logf("Could not create %s, reflection warnings may not be disabled: %v", configFile, err)
}
return nil
}
func mvnInstalled(ctx *gcp.Context) (bool, error) {
result, err := ctx.Exec([]string{"bash", "-c", "command -v mvn || true"})
if err != nil {
return false, err
}
return result.Stdout != "", nil
}
// installMaven installs Maven and returns the path of the mvn binary
func installMaven(ctx *gcp.Context) (string, error) {
mvnl, err := ctx.Layer(mavenLayer, gcp.CacheLayer, gcp.BuildLayer, gcp.LaunchLayerIfDevMode)
if err != nil {
return "", fmt.Errorf("creating %v layer: %w", mavenLayer, err)
}
// Check the metadata in the cache layer to determine if we need to proceed.
metaVersion := ctx.GetMetadata(mvnl, versionKey)
if mavenVersion == metaVersion {
ctx.CacheHit(mavenLayer)
ctx.Logf("Maven cache hit, skipping installation.")
return filepath.Join(mvnl.Path, "bin", "mvn"), nil
}
ctx.CacheMiss(mavenLayer)
if err := ctx.ClearLayer(mvnl); err != nil {
return "", fmt.Errorf("clearing layer %q: %w", mvnl.Name, err)
}
// Download and install maven in layer.
ctx.Logf("Installing Maven v%s", mavenVersion)
archiveURL := fmt.Sprintf(mavenURL, mavenVersion)
code, err := ctx.HTTPStatus(archiveURL)
if err != nil {
return "", err
}
if code != http.StatusOK {
return "", gcp.InternalErrorf("Maven version %s does not exist at %s (status %d).", mavenVersion, archiveURL, code)
}
command := fmt.Sprintf("curl --fail --show-error --silent --location --retry 3 %s | tar xz --directory %s --strip-components=1", archiveURL, mvnl.Path)
if _, err := ctx.Exec([]string{"bash", "-c", command}); err != nil {
return "", err
}
ctx.SetMetadata(mvnl, versionKey, mavenVersion)
return filepath.Join(mvnl.Path, "bin", "mvn"), nil
}
func pomFilePath(ctx *gcp.Context) (string, error) {
buildable := os.Getenv(env.Buildable)
pomPath := filepath.Join(buildable, "pom.xml")
pomExists, err := ctx.FileExists(pomPath)
if err != nil {
return "", err
}
if pomExists {
return pomPath, nil
}
return "", nil
}