cli/azd/pkg/tools/maven/maven.go (225 lines of code) (raw):
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package maven
import (
"bufio"
"context"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
osexec "os/exec"
"github.com/azure/azure-dev/cli/azd/pkg/exec"
"github.com/azure/azure-dev/cli/azd/pkg/tools"
)
var _ tools.ExternalTool = (*Cli)(nil)
type Cli struct {
commandRunner exec.CommandRunner
projectPath string
rootProjectPath string
// Lazily initialized. Access through mvnCmd.
mvnCmdStr string
mvnCmdOnce sync.Once
mvnCmdErr error
}
func (m *Cli) Name() string {
return "Maven"
}
func (m *Cli) InstallUrl() string {
return "https://maven.apache.org"
}
func (m *Cli) CheckInstalled(ctx context.Context) error {
_, err := m.mvnCmd()
if err != nil {
return err
}
if ver, err := m.extractVersion(ctx); err == nil {
log.Printf("maven version: %s", ver)
}
return nil
}
func (m *Cli) SetPath(projectPath string, rootProjectPath string) {
m.projectPath = projectPath
m.rootProjectPath = rootProjectPath
}
func (m *Cli) mvnCmd() (string, error) {
m.mvnCmdOnce.Do(func() {
mvnCmd, err := getMavenPath(m.projectPath, m.rootProjectPath)
if err != nil {
m.mvnCmdErr = err
} else {
m.mvnCmdStr = mvnCmd
}
})
if m.mvnCmdErr != nil {
return "", m.mvnCmdErr
}
return m.mvnCmdStr, nil
}
func getMavenPath(projectPath string, rootProjectPath string) (string, error) {
mvnw, err := getMavenWrapperPath(projectPath, rootProjectPath)
if mvnw != "" {
return mvnw, nil
}
if err != nil {
return "", fmt.Errorf("failed finding mvnw in repository path: %w", err)
}
mvn, err := osexec.LookPath("mvn")
if err == nil {
return mvn, nil
}
if !errors.Is(err, osexec.ErrNotFound) {
return "", fmt.Errorf("failed looking up mvn in PATH: %w", err)
}
return "", errors.New(
"maven could not be found. Install either Maven or Maven Wrapper by " +
"visiting https://maven.apache.org/ or https://maven.apache.org/wrapper/",
)
}
// getMavenWrapperPath finds the path to mvnw in the project directory, up to the root project directory.
//
// An error is returned if an unexpected error occurred while finding.
// If mvnw is not found, an empty string is returned with
// no error.
func getMavenWrapperPath(projectPath string, rootProjectPath string) (string, error) {
searchDir, err := filepath.Abs(projectPath)
if err != nil {
return "", err
}
root, err := filepath.Abs(rootProjectPath)
log.Printf("root: %s\n", root)
if err != nil {
return "", err
}
for {
log.Printf("searchDir: %s\n", searchDir)
mvnw, err := osexec.LookPath(filepath.Join(searchDir, "mvnw"))
if err == nil {
log.Printf("found mvnw as: %s\n", mvnw)
return mvnw, nil
}
if !errors.Is(err, os.ErrNotExist) && !errors.Is(err, osexec.ErrNotFound) {
return "", err
}
searchDir = filepath.Dir(searchDir)
// Past root, terminate search and return not found
if len(searchDir) < len(root) {
return "", nil
}
}
}
// mavenVersionRegexp captures the version number of maven from the output of "mvn --version"
//
// the output of mvn --version looks something like this:
// Apache Maven 3.9.1 (2e178502fcdbffc201671fb2537d0cb4b4cc58f8)
// Maven home: C:\Tools\apache-maven-3.9.1
// Java version: 17.0.6, vendor: Microsoft, runtime: C:\Program Files\Microsoft\jdk-17.0.6.10-hotspot
// Default locale: en_US, platform encoding: Cp1252
// OS name: "windows 11", version: "10.0", arch: "amd64", family: "windows"
var mavenVersionRegexp = regexp.MustCompile(`Apache Maven (.*) \(`)
func (cli *Cli) extractVersion(ctx context.Context) (string, error) {
mvnCmd, err := cli.mvnCmd()
if err != nil {
return "", err
}
runArgs := exec.NewRunArgs(mvnCmd, "--version")
res, err := cli.commandRunner.Run(ctx, runArgs)
if err != nil {
return "", fmt.Errorf("failed to run %s --version: %w", mvnCmd, err)
}
parts := mavenVersionRegexp.FindStringSubmatch(res.Stdout)
if len(parts) != 2 {
return "", fmt.Errorf("could not parse %s --version output, did not match expected format", mvnCmd)
}
return parts[1], nil
}
func (cli *Cli) Compile(ctx context.Context, projectPath string) error {
mvnCmd, err := cli.mvnCmd()
if err != nil {
return err
}
runArgs := exec.NewRunArgs(mvnCmd, "compile").WithCwd(projectPath)
_, err = cli.commandRunner.Run(ctx, runArgs)
if err != nil {
return fmt.Errorf("mvn compile on project '%s' failed: %w", projectPath, err)
}
return nil
}
func (cli *Cli) Package(ctx context.Context, projectPath string) error {
mvnCmd, err := cli.mvnCmd()
if err != nil {
return err
}
// Maven's package phase includes tests by default. Skip it explicitly.
runArgs := exec.NewRunArgs(mvnCmd, "package", "-DskipTests").WithCwd(projectPath)
_, err = cli.commandRunner.Run(ctx, runArgs)
if err != nil {
return fmt.Errorf("mvn package on project '%s' failed: %w", projectPath, err)
}
return nil
}
func (cli *Cli) ResolveDependencies(ctx context.Context, projectPath string) error {
mvnCmd, err := cli.mvnCmd()
if err != nil {
return err
}
runArgs := exec.NewRunArgs(mvnCmd, "dependency:resolve").WithCwd(projectPath)
_, err = cli.commandRunner.Run(ctx, runArgs)
if err != nil {
return fmt.Errorf("mvn dependency:resolve on project '%s' failed: %w", projectPath, err)
}
return nil
}
var ErrPropertyNotFound = errors.New("property not found")
func (cli *Cli) GetProperty(ctx context.Context, propertyPath string, projectPath string) (string, error) {
mvnCmd, err := cli.mvnCmd()
if err != nil {
return "", err
}
runArgs := exec.NewRunArgs(mvnCmd,
"help:evaluate",
// cspell: disable-next-line Dexpression and DforceStdout are maven command line arguments
"-Dexpression="+propertyPath, "-q", "-DforceStdout").WithCwd(projectPath)
res, err := cli.commandRunner.Run(ctx, runArgs)
if err != nil {
return "", fmt.Errorf("mvn help:evaluate on project '%s' failed: %w", projectPath, err)
}
result := strings.TrimSpace(res.Stdout)
if result == "null object or invalid expression" {
return "", ErrPropertyNotFound
}
return result, nil
}
func (cli *Cli) EffectivePom(ctx context.Context, pomPath string) (string, error) {
mvnCmd, err := cli.mvnCmd()
if err != nil {
return "", err
}
pomDir := filepath.Dir(pomPath)
// Link to "-pl" related doc: https://maven.apache.org/ref/3.1.0/maven-embedder/cli.html
runArgs := exec.NewRunArgs(mvnCmd, "help:effective-pom", "-f", pomPath, "-pl", filepath.Base(pomPath)).WithCwd(pomDir)
result, err := cli.commandRunner.Run(ctx, runArgs)
if err != nil {
return "", fmt.Errorf("mvn help:effective-pom on project '%s' failed: %w", pomPath, err)
}
return getEffectivePomStringFromConsoleOutput(result.Stdout)
}
var projectStart = regexp.MustCompile(`^\s*<project `) // the space can not be deleted.
var projectEnd = regexp.MustCompile(`^\s*</project>\s*$`)
func getEffectivePomStringFromConsoleOutput(consoleOutput string) (string, error) {
var builder strings.Builder
scanner := bufio.NewScanner(strings.NewReader(consoleOutput))
projectStarted := false
projectEnded := false
for scanner.Scan() {
line := scanner.Text()
if projectStart.MatchString(line) {
projectStarted = true
} else if projectEnd.MatchString(line) {
projectEnded = true
}
if projectStarted {
builder.WriteString(line)
}
if projectEnded {
break
}
}
if err := scanner.Err(); err != nil {
return "", fmt.Errorf("failed to scan console output: %w", err)
}
result := builder.String()
if result == "" {
return "", fmt.Errorf("failed to get effective pom from console: empty content")
}
return result, nil
}
func NewCli(commandRunner exec.CommandRunner) *Cli {
return &Cli{
commandRunner: commandRunner,
}
}