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 }