pkg/util/maven/maven_command.go (251 lines of code) (raw):
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You 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 maven
import (
"context"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"github.com/apache/camel-k/v2/pkg/util"
"github.com/apache/camel-k/v2/pkg/util/log"
)
var Log = log.WithName("maven")
type Command struct {
context Context
project Project
}
// Do is in charge to execute a given Maven phase.
func (c *Command) Do(ctx context.Context) error {
mvnCmd, err := c.mvnCmd(ctx)
if err != nil {
return err
}
mavenOptions, env := c.optionsFromEnv()
cmd := exec.CommandContext(ctx, mvnCmd, c.context.AdditionalArguments...)
cmd.Dir = c.context.Path
cmd.Env = env
Log.WithValues("MAVEN_OPTS", mavenOptions).Infof("executing: %s", strings.Join(cmd.Args, " "))
return util.RunAndLog(ctx, cmd, LogHandler, LogHandler)
}
// DoPom is in charge to generate the pom file.
func (c *Command) DoPom(ctx context.Context) error {
return generateProjectPom(c.context, c.project)
}
// DoSettings is in charge to prepare the maven settings required for a maven project.
func (c *Command) DoSettings(ctx context.Context) error {
if err := generateProjectSettings(c.context); err != nil {
return err
}
args := make([]string, 0)
args = append(args, c.context.AdditionalArguments...)
if c.context.LocalRepository != "" {
if _, err := os.Stat(c.context.LocalRepository); err == nil {
args = append(args, "-Dmaven.repo.local="+c.context.LocalRepository)
}
}
settingsPath := filepath.Join(c.context.Path, "settings.xml")
if settingsExists, err := util.FileExists(settingsPath); err != nil {
return err
} else if settingsExists {
args = append(args, "--global-settings", settingsPath)
}
settingsPath = filepath.Join(c.context.Path, "user-settings.xml")
if settingsExists, err := util.FileExists(settingsPath); err != nil {
return err
} else if settingsExists {
args = append(args, "--settings", settingsPath)
}
settingsSecurityPath := filepath.Join(c.context.Path, "settings-security.xml")
if settingsSecurityExists, err := util.FileExists(settingsSecurityPath); err != nil {
return err
} else if settingsSecurityExists {
args = append(args, "-Dsettings.security="+settingsSecurityPath)
}
// generate maven file
if !c.context.SkipMavenConfigGeneration {
if err := generateMavenContext(c.context.Path, args, c.context.ExtraMavenOpts); err != nil {
return err
}
}
return nil
}
func (c *Command) optionsFromEnv() ([]string, []string) {
if len(c.context.ExtraMavenOpts) == 0 {
return nil, nil
}
var mavenOptions string
// Inherit the parent process environment
env := os.Environ()
mavenOpts, ok := os.LookupEnv("MAVEN_OPTS")
if !ok {
mavenOptions = strings.Join(c.context.ExtraMavenOpts, " ")
env = append(env, "MAVEN_OPTS="+mavenOptions)
} else {
var extraOptions []string
options := strings.Fields(mavenOpts)
for _, extraOption := range c.context.ExtraMavenOpts {
// Basic duplicated key detection, that should be improved
// to support a wider range of JVM options
key := strings.SplitN(extraOption, "=", 2)[0]
exists := false
for _, opt := range options {
if strings.HasPrefix(opt, key) {
exists = true
break
}
}
if !exists {
extraOptions = append(extraOptions, extraOption)
}
}
options = append(options, extraOptions...)
mavenOptions = strings.Join(options, " ")
for i, e := range env {
if strings.HasPrefix(e, "MAVEN_OPTS=") {
env[i] = "MAVEN_OPTS=" + mavenOptions
break
}
}
}
return c.context.ExtraMavenOpts, env
}
// mvnCmd prepares the maven wrapper on the maven project or just use any other maven command
// driven by MAVEN_CMD and MAVEN_WRAPPER environment variables configuration.
func (c *Command) mvnCmd(ctx context.Context) (string, error) {
mvnCmd := ""
if c, ok := os.LookupEnv("MAVEN_CMD"); ok {
mvnCmd = c
}
if mvnCmd == "" {
if e, ok := os.LookupEnv("MAVEN_WRAPPER"); (ok && e == "true") || !ok {
// Prepare maven wrapper helps when running the builder as Pod as it makes
// the builder container, Maven agnostic
if err := c.prepareMavenWrapper(ctx); err != nil {
return "", err
}
}
mvnCmd = "./mvnw"
}
return mvnCmd, nil
}
func NewContext(buildDir string) Context {
return Context{
Path: buildDir,
AdditionalArguments: make([]string, 0),
AdditionalEntries: make(map[string]interface{}),
}
}
type Context struct {
SkipMavenConfigGeneration bool
Path string
ExtraMavenOpts []string
GlobalSettings []byte
UserSettings []byte
SettingsSecurity []byte
AdditionalArguments []string
AdditionalEntries map[string]interface{}
LocalRepository string
}
func (c *Context) AddEntry(id string, entry interface{}) {
if c.AdditionalEntries == nil {
c.AdditionalEntries = make(map[string]interface{})
}
c.AdditionalEntries[id] = entry
}
func (c *Context) AddArgument(argument string) {
c.AdditionalArguments = append(c.AdditionalArguments, argument)
}
func (c *Context) AddArgumentf(format string, args ...interface{}) {
c.AdditionalArguments = append(c.AdditionalArguments, fmt.Sprintf(format, args...))
}
func (c *Context) AddArguments(arguments ...string) {
c.AdditionalArguments = append(c.AdditionalArguments, arguments...)
}
func (c *Context) AddSystemProperty(name string, value string) {
c.AddArgumentf("-D%s=%s", name, value)
}
// generateProjectPom is in charge to generate the pom.xml of an "in-memory" Project type.
func generateProjectPom(context Context, project Project) error {
return util.WriteFileWithBytesMarshallerContent(context.Path, "pom.xml", &project)
}
// generateProjectSettings is in charge to generate the settings for any following maven command execution.
func generateProjectSettings(context Context) error {
if context.GlobalSettings != nil {
if err := util.WriteFileWithContent(filepath.Join(context.Path, "settings.xml"), context.GlobalSettings); err != nil {
return err
}
}
if context.UserSettings != nil {
if err := util.WriteFileWithContent(filepath.Join(context.Path, "user-settings.xml"), context.UserSettings); err != nil {
return err
}
}
if context.SettingsSecurity != nil {
if err := util.WriteFileWithContent(filepath.Join(context.Path, "settings-security.xml"), context.SettingsSecurity); err != nil {
return err
}
}
for k, v := range context.AdditionalEntries {
var bytes []byte
var err error
if dc, ok := v.([]byte); ok {
bytes = dc
} else if dc, ok := v.(io.Reader); ok {
bytes, err = io.ReadAll(dc)
if err != nil {
return err
}
} else {
return fmt.Errorf("unknown content type: name=%s, content=%+v", k, v)
}
if len(bytes) > 0 {
Log.Infof("write entry: %s (%d bytes)", k, len(bytes))
err = util.WriteFileWithContent(filepath.Join(context.Path, k), bytes)
if err != nil {
return err
}
}
}
return nil
}
// We expect a maven wrapper under /usr/share/maven/mvnw.
func (c *Command) prepareMavenWrapper(ctx context.Context) error {
cmd := exec.CommandContext(ctx, "cp", "--recursive", "/usr/share/maven/mvnw/.", ".")
cmd.Dir = c.context.Path
return util.RunAndLog(ctx, cmd, LogHandler, LogHandler)
}
// ParseGAV decodes the provided Maven GAV into the corresponding Dependency.
//
// The artifact id is in the form of:
//
// <groupId>:<artifactId>[:<packagingType>]:(<version>)[:<classifier>]
//
//nolint:mnd
func ParseGAV(gav string) (Dependency, error) {
dep := Dependency{}
res := strings.Split(gav, ":")
count := len(res)
if res == nil || count < 2 {
return Dependency{}, errors.New("GAV must match <groupId>:<artifactId>[:<packagingType>]:(<version>)[:<classifier>]")
}
dep.GroupID = res[0]
dep.ArtifactID = res[1]
switch {
case count == 3:
// gav is: org:artifact:<type:version>
numeric := regexp.MustCompile(`\d`)
if numeric.MatchString(res[2]) {
dep.Version = res[2]
} else {
dep.Type = res[2]
}
case count == 4:
// gav is: org:artifact:type:version
dep.Type = res[2]
dep.Version = res[3]
case count == 5:
// gav is: org:artifact:<type>:<version>:classifier
dep.Type = res[2]
dep.Version = res[3]
dep.Classifier = res[4]
}
return dep, nil
}
// Create a .mvn/maven.config file containing all arguments for any follow up maven command.
func generateMavenContext(path string, args, options []string) error {
return util.WriteFileWithContent(filepath.Join(path, ".mvn", "maven.config"), []byte(getMavenContext(args, options)))
}
func getMavenContext(args, options []string) string {
mavenContext := ""
for _, arg := range args {
arg = strings.TrimSpace(arg)
if arg != "package" && len(arg) != 0 {
mavenContext += fmt.Sprintf("%s\n", arg)
}
}
for _, opt := range options {
opt = strings.TrimSpace(opt)
if len(opt) != 0 {
mavenContext += fmt.Sprintf("%s\n", opt)
}
}
return mavenContext
}