github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/pkg/maven/maven.go (about)

     1  package maven
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/SAP/jenkins-library/pkg/command"
    13  	piperhttp "github.com/SAP/jenkins-library/pkg/http"
    14  	"github.com/SAP/jenkins-library/pkg/piperutils"
    15  
    16  	"github.com/SAP/jenkins-library/pkg/log"
    17  )
    18  
    19  // ExecuteOptions are used by Execute() to construct the Maven command line.
    20  type ExecuteOptions struct {
    21  	PomPath                     string   `json:"pomPath,omitempty"`
    22  	ProjectSettingsFile         string   `json:"projectSettingsFile,omitempty"`
    23  	GlobalSettingsFile          string   `json:"globalSettingsFile,omitempty"`
    24  	M2Path                      string   `json:"m2Path,omitempty"`
    25  	Goals                       []string `json:"goals,omitempty"`
    26  	Defines                     []string `json:"defines,omitempty"`
    27  	Flags                       []string `json:"flags,omitempty"`
    28  	LogSuccessfulMavenTransfers bool     `json:"logSuccessfulMavenTransfers,omitempty"`
    29  	ReturnStdout                bool     `json:"returnStdout,omitempty"`
    30  }
    31  
    32  // EvaluateOptions are used by Evaluate() to construct the Maven command line.
    33  // In contrast to ExecuteOptions, fewer settings are required for Evaluate and thus a separate type is needed.
    34  type EvaluateOptions struct {
    35  	PomPath             string   `json:"pomPath,omitempty"`
    36  	ProjectSettingsFile string   `json:"projectSettingsFile,omitempty"`
    37  	GlobalSettingsFile  string   `json:"globalSettingsFile,omitempty"`
    38  	M2Path              string   `json:"m2Path,omitempty"`
    39  	Defines             []string `json:"defines,omitempty"`
    40  }
    41  
    42  type Utils interface {
    43  	Stdout(out io.Writer)
    44  	Stderr(err io.Writer)
    45  	RunExecutable(e string, p ...string) error
    46  
    47  	DownloadFile(url, filename string, header http.Header, cookies []*http.Cookie) error
    48  	Glob(pattern string) (matches []string, err error)
    49  	FileExists(filename string) (bool, error)
    50  	Copy(src, dest string) (int64, error)
    51  	MkdirAll(path string, perm os.FileMode) error
    52  	FileWrite(path string, content []byte, perm os.FileMode) error
    53  	FileRead(path string) ([]byte, error)
    54  }
    55  
    56  type utilsBundle struct {
    57  	*command.Command
    58  	*piperutils.Files
    59  	*piperhttp.Client
    60  }
    61  
    62  func NewUtilsBundle() Utils {
    63  	utils := utilsBundle{
    64  		Command: &command.Command{},
    65  		Files:   &piperutils.Files{},
    66  		Client:  &piperhttp.Client{},
    67  	}
    68  	utils.Stdout(log.Writer())
    69  	utils.Stderr(log.Writer())
    70  	return &utils
    71  }
    72  
    73  const mavenExecutable = "mvn"
    74  
    75  // Execute constructs a mvn command line from the given options, and uses the provided
    76  // mavenExecRunner to execute it.
    77  func Execute(options *ExecuteOptions, utils Utils) (string, error) {
    78  	stdOutBuf, stdOut := evaluateStdOut(options)
    79  	utils.Stdout(stdOut)
    80  	utils.Stderr(log.Writer())
    81  
    82  	parameters, err := getParametersFromOptions(options, utils)
    83  	if err != nil {
    84  		return "", fmt.Errorf("failed to construct parameters from options: %w", err)
    85  	}
    86  
    87  	err = utils.RunExecutable(mavenExecutable, parameters...)
    88  	if err != nil {
    89  		log.SetErrorCategory(log.ErrorBuild)
    90  		commandLine := append([]string{mavenExecutable}, parameters...)
    91  		return "", fmt.Errorf("failed to run executable, command: '%s', error: %w", commandLine, err)
    92  	}
    93  
    94  	if stdOutBuf == nil {
    95  		return "", nil
    96  	}
    97  	return string(stdOutBuf.Bytes()), nil
    98  }
    99  
   100  // Evaluate constructs ExecuteOptions for using the maven-help-plugin's 'evaluate' goal to
   101  // evaluate a given expression from a pom file. This allows to retrieve the value of - for
   102  // example - 'project.version' from a pom file exactly as Maven itself evaluates it.
   103  func Evaluate(options *EvaluateOptions, expression string, utils Utils) (string, error) {
   104  	defines := []string{"-Dexpression=" + expression, "-DforceStdout", "-q"}
   105  	defines = append(defines, options.Defines...)
   106  	executeOptions := ExecuteOptions{
   107  		PomPath:             options.PomPath,
   108  		M2Path:              options.M2Path,
   109  		ProjectSettingsFile: options.ProjectSettingsFile,
   110  		GlobalSettingsFile:  options.GlobalSettingsFile,
   111  		Goals:               []string{"org.apache.maven.plugins:maven-help-plugin:3.1.0:evaluate"},
   112  		Defines:             defines,
   113  		ReturnStdout:        true,
   114  	}
   115  	value, err := Execute(&executeOptions, utils)
   116  	if err != nil {
   117  		return "", err
   118  	}
   119  	if strings.HasPrefix(value, "null object or invalid expression") {
   120  		return "", fmt.Errorf("expression '%s' in file '%s' could not be resolved", expression, options.PomPath)
   121  	}
   122  	return value, nil
   123  }
   124  
   125  // InstallFile installs a maven artifact and its pom into the local maven repository.
   126  // If "file" is empty, only the pom is installed. "pomFile" must not be empty.
   127  func InstallFile(file, pomFile string, options *EvaluateOptions, utils Utils) error {
   128  	if len(pomFile) == 0 {
   129  		return fmt.Errorf("pomFile can't be empty")
   130  	}
   131  
   132  	var defines []string
   133  	if len(file) > 0 {
   134  		defines = append(defines, "-Dfile="+file)
   135  		if strings.Contains(file, ".jar") {
   136  			defines = append(defines, "-Dpackaging=jar")
   137  		}
   138  		if strings.Contains(file, "-classes") {
   139  			defines = append(defines, "-Dclassifier=classes")
   140  		}
   141  
   142  	} else {
   143  		defines = append(defines, "-Dfile="+pomFile)
   144  	}
   145  	defines = append(defines, "-DpomFile="+pomFile)
   146  	mavenOptionsInstall := ExecuteOptions{
   147  		Goals:               []string{"install:install-file"},
   148  		Defines:             defines,
   149  		M2Path:              options.M2Path,
   150  		ProjectSettingsFile: options.ProjectSettingsFile,
   151  		GlobalSettingsFile:  options.GlobalSettingsFile,
   152  	}
   153  	_, err := Execute(&mavenOptionsInstall, utils)
   154  	if err != nil {
   155  		return fmt.Errorf("failed to install maven artifacts: %w", err)
   156  	}
   157  	return nil
   158  }
   159  
   160  // InstallMavenArtifacts finds maven modules (identified by pom.xml files) and installs the artifacts into the local maven repository.
   161  func InstallMavenArtifacts(options *EvaluateOptions, utils Utils) error {
   162  	return doInstallMavenArtifacts(options, utils)
   163  }
   164  
   165  func doInstallMavenArtifacts(options *EvaluateOptions, utils Utils) error {
   166  	err := flattenPom(options, utils)
   167  	if err != nil {
   168  		return err
   169  	}
   170  
   171  	pomFiles, err := utils.Glob(filepath.Join("**", "pom.xml"))
   172  	if err != nil {
   173  		return err
   174  	}
   175  
   176  	// Ensure m2 path is an absolute path, even if it is given relative
   177  	// This is important to avoid getting multiple m2 directories in a maven multimodule project
   178  	if options.M2Path != "" {
   179  		options.M2Path, err = filepath.Abs(options.M2Path)
   180  		if err != nil {
   181  			return err
   182  		}
   183  	}
   184  
   185  	for _, pomFile := range pomFiles {
   186  		log.Entry().Info("Installing maven artifacts from module: " + pomFile)
   187  
   188  		// Set this module's pom file as the pom file for evaluating the packaging,
   189  		// otherwise we would evaluate the root pom in all iterations.
   190  		evaluateProjectPackagingOptions := *options
   191  		evaluateProjectPackagingOptions.PomPath = pomFile
   192  		packaging, err := Evaluate(&evaluateProjectPackagingOptions, "project.packaging", utils)
   193  		if err != nil {
   194  			return err
   195  		}
   196  
   197  		currentModuleDir := filepath.Dir(pomFile)
   198  
   199  		// Use flat pom if available to avoid issues with unresolved variables.
   200  		pathToPomFile := pomFile
   201  		flattenedPomExists, _ := utils.FileExists(filepath.Join(currentModuleDir, ".flattened-pom.xml"))
   202  		if flattenedPomExists {
   203  			pathToPomFile = filepath.Join(currentModuleDir, ".flattened-pom.xml")
   204  		}
   205  
   206  		if packaging == "pom" {
   207  			err = InstallFile("", pathToPomFile, options, utils)
   208  			if err != nil {
   209  				return err
   210  			}
   211  		} else {
   212  
   213  			err = installJarWarArtifacts(pathToPomFile, currentModuleDir, options, utils)
   214  			if err != nil {
   215  				return err
   216  			}
   217  		}
   218  	}
   219  	return err
   220  }
   221  
   222  func installJarWarArtifacts(pomFile, dir string, options *EvaluateOptions, utils Utils) error {
   223  	options.PomPath = filepath.Join(dir, "pom.xml")
   224  	finalName, err := Evaluate(options, "project.build.finalName", utils)
   225  	if err != nil {
   226  		return err
   227  	}
   228  	if finalName == "" {
   229  		log.Entry().Warn("project.build.finalName is empty, skipping install of artifact. Installing only the pom file.")
   230  		err = InstallFile("", pomFile, options, utils)
   231  		if err != nil {
   232  			return err
   233  		}
   234  		return nil
   235  	}
   236  
   237  	jarExists, _ := utils.FileExists(jarFile(dir, finalName))
   238  	warExists, _ := utils.FileExists(warFile(dir, finalName))
   239  	classesJarExists, _ := utils.FileExists(classesJarFile(dir, finalName))
   240  	originalJarExists, _ := utils.FileExists(originalJarFile(dir, finalName))
   241  
   242  	log.Entry().Infof("JAR file with name %s does exist: %t", jarFile(dir, finalName), jarExists)
   243  	log.Entry().Infof("Classes-JAR file with name %s does exist: %t", classesJarFile(dir, finalName), classesJarExists)
   244  	log.Entry().Infof("Original-JAR file with name %s does exist: %t", originalJarFile(dir, finalName), originalJarExists)
   245  	log.Entry().Infof("WAR file with name %s does exist: %t", warFile(dir, finalName), warExists)
   246  
   247  	// Due to spring's jar repackaging we need to check for an "original" jar file because the repackaged one is no suitable source for dependent maven modules
   248  	if originalJarExists {
   249  		err = InstallFile(originalJarFile(dir, finalName), pomFile, options, utils)
   250  		if err != nil {
   251  			return err
   252  		}
   253  	} else if jarExists {
   254  		err = InstallFile(jarFile(dir, finalName), pomFile, options, utils)
   255  		if err != nil {
   256  			return err
   257  		}
   258  	}
   259  
   260  	if warExists {
   261  		err = InstallFile(warFile(dir, finalName), pomFile, options, utils)
   262  		if err != nil {
   263  			return err
   264  		}
   265  	}
   266  
   267  	if classesJarExists {
   268  		err = InstallFile(classesJarFile(dir, finalName), pomFile, options, utils)
   269  		if err != nil {
   270  			return err
   271  		}
   272  	}
   273  	return nil
   274  }
   275  
   276  func jarFile(dir, finalName string) string {
   277  	return filepath.Join(dir, "target", finalName+".jar")
   278  }
   279  
   280  func classesJarFile(dir, finalName string) string {
   281  	return filepath.Join(dir, "target", finalName+"-classes.jar")
   282  }
   283  
   284  func originalJarFile(dir, finalName string) string {
   285  	return filepath.Join(dir, "target", finalName+".jar.original")
   286  }
   287  
   288  func warFile(dir, finalName string) string {
   289  	return filepath.Join(dir, "target", finalName+".war")
   290  }
   291  
   292  func flattenPom(options *EvaluateOptions, utils Utils) error {
   293  	mavenOptionsFlatten := ExecuteOptions{
   294  		Goals:               []string{"flatten:flatten"},
   295  		Defines:             []string{"-Dflatten.mode=resolveCiFriendliesOnly"},
   296  		PomPath:             options.PomPath,
   297  		M2Path:              options.M2Path,
   298  		ProjectSettingsFile: options.ProjectSettingsFile,
   299  		GlobalSettingsFile:  options.GlobalSettingsFile,
   300  	}
   301  	_, err := Execute(&mavenOptionsFlatten, utils)
   302  	return err
   303  }
   304  
   305  func evaluateStdOut(options *ExecuteOptions) (*bytes.Buffer, io.Writer) {
   306  	var stdOutBuf *bytes.Buffer
   307  	stdOut := log.Writer()
   308  	if options.ReturnStdout {
   309  		stdOutBuf = new(bytes.Buffer)
   310  		stdOut = io.MultiWriter(stdOut, stdOutBuf)
   311  	}
   312  	return stdOutBuf, stdOut
   313  }
   314  
   315  func getParametersFromOptions(options *ExecuteOptions, utils Utils) ([]string, error) {
   316  	var parameters []string
   317  
   318  	parameters, err := DownloadAndGetMavenParameters(options.GlobalSettingsFile, options.ProjectSettingsFile, utils)
   319  	if err != nil {
   320  		return nil, err
   321  	}
   322  
   323  	if options.M2Path != "" {
   324  		parameters = append(parameters, "-Dmaven.repo.local="+options.M2Path)
   325  	}
   326  
   327  	if options.PomPath != "" {
   328  		parameters = append(parameters, "--file", options.PomPath)
   329  	}
   330  
   331  	if options.Flags != nil {
   332  		parameters = append(parameters, options.Flags...)
   333  	}
   334  
   335  	if options.Defines != nil {
   336  		parameters = append(parameters, options.Defines...)
   337  	}
   338  
   339  	if !options.LogSuccessfulMavenTransfers {
   340  		parameters = append(parameters, "-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn")
   341  	}
   342  
   343  	parameters = append(parameters, "--batch-mode")
   344  
   345  	parameters = append(parameters, options.Goals...)
   346  
   347  	return parameters, nil
   348  }
   349  
   350  // GetTestModulesExcludes return testing modules that you be excluded from reactor
   351  func GetTestModulesExcludes(utils Utils) []string {
   352  	var excludes []string
   353  	exists, _ := utils.FileExists("unit-tests/pom.xml")
   354  	if exists {
   355  		excludes = append(excludes, "-pl", "!unit-tests")
   356  	}
   357  	exists, _ = utils.FileExists("integration-tests/pom.xml")
   358  	if exists {
   359  		excludes = append(excludes, "-pl", "!integration-tests")
   360  	}
   361  	return excludes
   362  }