pkg/java/java.go (157 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. // Package java contains Java buildpack library code. package java import ( "archive/zip" "fmt" "io/ioutil" "os" "path/filepath" "regexp" "strings" "time" "github.com/GoogleCloudPlatform/buildpacks/pkg/env" gcp "github.com/GoogleCloudPlatform/buildpacks/pkg/gcpbuildpack" "github.com/buildpacks/libcnb/v2" ) const ( dateFormat = time.RFC3339Nano // repoExpiration is an arbitrary amount of time of 10 weeks to refresh the cache layer. // TODO(b/148099877): Investigate proper cache-clearing strategy. repoExpiration = time.Duration(time.Hour * 24 * 7 * 10) // ManifestPath specifies the path of MANIFEST.MF relative to the working directory. ManifestPath = "META-INF/MANIFEST.MF" mainClassKey = "Main-Class" // manifestRegexTemplate is a regexp template that matches lines in the manifest for a given entry. manifestRegexTemplate = `(?m)^%s: \S+` expiryTimestampKey = "expiry_timestamp" // FFJarPathEnv is an environment variable which is used to store the path to the functions framework invoker jar. FFJarPathEnv = "GOOGLE_INTERNAL_FUNCTIONS_FRAMEWORK_JAR" // GradleBuildArgs is an env var used to append arguments to the gradle build command. // Example: `clean assemble` for Maven apps run "gradle clean assemble" command. GradleBuildArgs = "GOOGLE_GRADLE_BUILD_ARGS" // MavenBuildArgs is an env var used to append arguments to the mvn build command. // Example: `clean package` for Maven apps run "mvn clean package" command. MavenBuildArgs = "GOOGLE_MAVEN_BUILD_ARGS" ) var ( // jarPaths contains the paths that we search for executable jar files. Order of paths decides precedence. jarPaths = [][]string{ []string{"target"}, []string{"build"}, []string{"build", "libs"}, []string{"*", "build", "libs"}, // An empty file path searches the application root for jars. []string{}, } ) // ExecutableJar looks for the jar with a Main-Class manifest. If there is not exactly 1 of these jars, throw an error. func ExecutableJar(ctx *gcp.Context) (string, error) { var buildable = os.Getenv(env.Buildable) if buildable != "" { jarPaths = append([][]string{[]string{buildable, "target"}}, jarPaths...) } for i, path := range jarPaths { path = append([]string{ctx.ApplicationRoot()}, path...) path = append(path, "*.jar") jars, err := ctx.Glob(filepath.Join(path...)) if err != nil { return "", fmt.Errorf("finding jars: %w", err) } // There may be multiple jars due to some frameworks like Quarkus creating multiple jars, // so we look for the jar that contains a Main-Class entry in its manifest. executables := filterExecutables(ctx, jars) // We've found a path with exactly 1 jar, so return that jar. if len(executables) == 1 { return executables[0], nil } else if len(executables) > 1 { return "", gcp.UserErrorf("found more than one jar with a Main-Class manifest entry in %s: %v, please specify an entrypoint", jarPaths[i], executables) } } return "", gcp.UserErrorf("did not find any jar files with a Main-Class manifest entry") } func filterExecutables(ctx *gcp.Context, jars []string) []string { var executables []string for _, jar := range jars { if main, err := FindManifestValueFromJar(jar, mainClassKey); err != nil { ctx.Warnf("Failed to inspect %s, skipping: %v.", jar, err) } else if main != "" { executables = append(executables, jar) } } return executables } // MainManifestEntry returns the Main-Class manifest entry of the jar at the given filepath, // or an empty string if the entry does not exist. func MainManifestEntry(jar string) (string, error) { return FindManifestValueFromJar(jar, mainClassKey) } // FindManifestValueFromJar returns a manifest entry value from a JAR if found, or empty otherwise. func FindManifestValueFromJar(jarPath, key string) (string, error) { r, err := zip.OpenReader(jarPath) if err != nil { return "", gcp.UserErrorf("unzipping jar %s: %v", jarPath, err) } defer r.Close() for _, f := range r.File { if f.Name != ManifestPath { continue } rc, err := f.Open() if err != nil { return "", fmt.Errorf("opening file %s in jar %s: %v", f.FileInfo().Name(), jarPath, err) } content, err := ioutil.ReadAll(rc) if err != nil { return "", err } return findValueFromManifest(content, key) } return "", nil } // MainFromManifest returns the main class specified in the manifest at the input path. func MainFromManifest(ctx *gcp.Context, manifestPath string) (string, error) { content, err := ctx.ReadFile(manifestPath) if err != nil { return "", err } main, err := findValueFromManifest(content, mainClassKey) if err != nil { return "", err } if main == "" { return "", gcp.UserErrorf("no Main-Class manifest entry found in the manifest:\n%s", content) } return main, nil } func findValueFromManifest(manifestContent []byte, key string) (string, error) { reRaw := fmt.Sprintf(manifestRegexTemplate, key) re, err := regexp.Compile(reRaw) if err != nil { return "", fmt.Errorf("invalid manifest key unsuitable for regexp: %q, %w", key, err) } match := re.Find(manifestContent) if len(match) != 0 { return strings.TrimPrefix(string(match), key+": "), nil } return "", nil } // CheckCacheExpiration clears the m2 layer and sets a new expiry timestamp when the cache is past expiration. func CheckCacheExpiration(ctx *gcp.Context, m2CachedRepo *libcnb.Layer) error { t := time.Now() expiry := ctx.GetMetadata(m2CachedRepo, expiryTimestampKey) if expiry != "" { var err error t, err = time.Parse(dateFormat, expiry) if err != nil { ctx.Debugf("Could not parse expiration date %q, assuming now: %v", expiry, err) } } if t.After(time.Now()) { return nil } ctx.Debugf("Cache expired on %v, clearing", t) if err := ctx.ClearLayer(m2CachedRepo); err != nil { return fmt.Errorf("clearing layer %q: %w", m2CachedRepo.Name, err) } ctx.SetMetadata(m2CachedRepo, expiryTimestampKey, time.Now().Add(repoExpiration).Format(dateFormat)) return nil } // MvnCmd returns the command that should be used to invoke maven for this build. func MvnCmd(ctx *gcp.Context) (string, error) { exists, err := ctx.FileExists("mvnw") if err != nil { return "", err } // If this project has the Maven Wrapper, we should use it if exists { return "./mvnw", nil } return "mvn", nil } // GradleCmd returns the command that should be used to invoke gradle for this build. func GradleCmd(ctx *gcp.Context) (string, error) { exists, err := ctx.FileExists("gradlew") if err != nil { return "", err } // If this project has the Gradle Wrapper, we should use it if exists { return "./gradlew", nil } return "gradle", nil }