kythe.io@v0.0.68-0.20240422202219-7225dbc01741/kythe/go/extractors/config/preprocessor/modifier/pom_xml_modifier.go (about)

     1  /*
     2   * Copyright 2018 The Kythe Authors. All rights reserved.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *   http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package modifier
    18  
    19  import (
    20  	"fmt"
    21  	"os"
    22  	"strconv"
    23  	"strings"
    24  
    25  	"github.com/beevik/etree"
    26  )
    27  
    28  // PreProcessPomXML takes a pom.xml file and either verifies that it already has
    29  // the bits necessary to specify a separate compiler on commandline, or adds
    30  // functionality by dropping in a maven-compiler-plugin to the build.
    31  //
    32  // Note this potentially overwrites the input file, even if it returns an error,
    33  // so make a copy beforehand if you need to keep the original.
    34  func PreProcessPomXML(pomXMLFile string) error {
    35  	doc := etree.NewDocument()
    36  	err := doc.ReadFromFile(pomXMLFile)
    37  	if err != nil {
    38  		return fmt.Errorf("reading XML file %s: %v", pomXMLFile, err)
    39  	}
    40  	hasPlugin, err := hasCompilerPlugin(doc)
    41  	if err != nil {
    42  		return err
    43  	}
    44  	if hasPlugin {
    45  		return nil
    46  	}
    47  	if err := insertCompilerPlugin(doc); err != nil {
    48  		return fmt.Errorf("adding maven-compiler-plugin: %v", err)
    49  	}
    50  	f, err := os.OpenFile(pomXMLFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
    51  	if err != nil {
    52  		return fmt.Errorf("opening file: %v", err)
    53  	}
    54  	doc.Indent(2)
    55  	doc.WriteTo(f)
    56  	return f.Close()
    57  }
    58  
    59  // hasCompilerPLugin returns whether there is a maven-compiler-plugin specified
    60  // already.  Additionally return whether or not we optimistically think that any
    61  // specified maven-compiler-plugin will work for us, or if we can see obvious
    62  // errors.
    63  func hasCompilerPlugin(doc *etree.Document) (bool, error) {
    64  	for _, p := range doc.FindElements("//project/build/plugins/plugin") {
    65  		a := p.FindElement("artifactId")
    66  		if a.Text() == "maven-compiler-plugin" {
    67  			// Now, we either check for the existence of correctly-specified
    68  			// elements for groupId, version, and
    69  			// configuration/[source|target], or we check for the complete
    70  			// absence of all those elements.  For the former we check
    71  			// validity, and for the latter we just assume that there's some
    72  			// well-formed parent pluginManagement specifying the values.
    73  			if len(p.ChildElements()) == 1 {
    74  				// Assume that there's some well-formed parent
    75  				// pluginManagement specifying the values.
    76  				// TODO(danielmoy): handle the case where pom.xml plugin is
    77  				// incorrectly specified, by looking at all of the pom.xml
    78  				// files in the whole project wholistically.  This might be
    79  				// out of scope - we might insist they be fixed upstream.
    80  				return true, nil
    81  			}
    82  			return true, wellformedCompilerPlugin(p)
    83  		}
    84  	}
    85  	for _, p := range doc.FindElements("//project/build/pluginManagement/plugins/plugin") {
    86  		a := p.FindElement("artifactId")
    87  		if a.Text() == "maven-compiler-plugin" {
    88  			// For pluginManagement, we do need a well-specified set of
    89  			// sibling elements.
    90  			return true, wellformedCompilerPlugin(p)
    91  		}
    92  	}
    93  
    94  	// If we didn't find anything, we can proceed with modification.
    95  	return false, nil
    96  }
    97  
    98  // wellformedCompilerPlugin checks to see if the plugin fits the minimum
    99  // supported versions we've tested:
   100  // <plugin>
   101  //
   102  //	<artifactId>maven-compiler-plugin</artifactId>
   103  //	<version>3.7.0</version>
   104  //	<configuration>
   105  //	  <source>1.8</source>
   106  //	  <target>1.8</target>
   107  //	</configuration>
   108  //
   109  // </plugin>
   110  // TODO(danielmoy): see if we can relax some of these restrictions.
   111  func wellformedCompilerPlugin(el *etree.Element) error {
   112  	v := el.FindElement("version")
   113  	if v == nil {
   114  		return fmt.Errorf("missing maven-compiler-plugin version")
   115  	}
   116  	vt, err := semvar(v.Text())
   117  	if err != nil || vt.earlierThan(versiontuple{3, 7, 0}) {
   118  		return fmt.Errorf("unsupported maven-compiler-plugin version: %s, need 3.7.0+", v.Text())
   119  	}
   120  
   121  	if err := checkJavaVersion(el, "source"); err != nil {
   122  		return err
   123  	}
   124  	if err := checkJavaVersion(el, "target"); err != nil {
   125  		return err
   126  	}
   127  
   128  	return nil
   129  }
   130  
   131  func checkJavaVersion(el *etree.Element, kind string) error {
   132  	k := el.FindElement(fmt.Sprintf("configuration/%s", kind))
   133  	if k == nil {
   134  		return fmt.Errorf("mising %s", kind)
   135  	}
   136  	if v, err := javaVersion(k.Text()); err != nil || v.earlierThan(versiontuple{1, 7}) {
   137  		// TODO(#3075): Should test an upper bound here too.
   138  		return fmt.Errorf("unsupported %s version: %s, need 1.7+", kind, k.Text())
   139  	}
   140  	return nil
   141  }
   142  
   143  // Some container that supports semvar (3.7.0) and java versions (1.6) for
   144  // comparison purposes only.
   145  type versiontuple []int
   146  
   147  func semvar(st string) (versiontuple, error) {
   148  	return fromString(st, 3)
   149  }
   150  
   151  func javaVersion(st string) (versiontuple, error) {
   152  	return fromString(st, 2)
   153  }
   154  
   155  func fromString(st string, size int) (versiontuple, error) {
   156  	v := versiontuple{}
   157  	bits := strings.Split(st, ".")
   158  	if len(bits) != size {
   159  		return v, fmt.Errorf("version should have %d parts separated by '.', got: %s", size, st)
   160  	}
   161  	for i := 0; i < size; i++ {
   162  		id, err := strconv.Atoi(bits[i])
   163  		if err != nil {
   164  			return v, fmt.Errorf("version %s has malformed index %d: %v", st, i, err)
   165  		}
   166  		v = append(v, id)
   167  	}
   168  	return v, nil
   169  }
   170  
   171  func (vt versiontuple) earlierThan(other versiontuple) bool {
   172  	// Version tuples aren't of the same length, probably shouldn't be
   173  	// compared
   174  	if len(vt) != len(other) {
   175  		return false
   176  	}
   177  	for i := 0; i < len(vt); i++ {
   178  		if vt[i] < other[i] {
   179  			return true
   180  		} else if vt[i] > other[i] {
   181  			return false
   182  		}
   183  	}
   184  	return false
   185  }
   186  
   187  // appendCompilerPlugin modifies a doc containing the root element of a pom.xml
   188  // configuration, by injecting a custom Maven compiler <plugin> element into the
   189  // plugin list for the top-level project.  This allows mvn install commands to
   190  // specify --Dmaven.compiler.executable on commandline and use a separate
   191  // compiler.
   192  //
   193  // Note this is unlike the build.gradle modification, where a separate
   194  // commandline argument is not necessary.
   195  //
   196  // An example modification can be found in the tests.  We would expect
   197  // testdata/other-pom.xml to be transformed into testdata/modified-pom.xml
   198  func insertCompilerPlugin(doc *etree.Document) error {
   199  	project := doc.SelectElement("project")
   200  	if project == nil {
   201  		return fmt.Errorf("no top level <project> element")
   202  	}
   203  	build := project.SelectElement("build")
   204  	if build == nil {
   205  		build = project.CreateElement("build")
   206  	}
   207  	plugins := build.SelectElement("plugins")
   208  	if plugins == nil {
   209  		plugins = build.CreateElement("plugins")
   210  	}
   211  	newPlugin := plugins.CreateElement("plugin")
   212  	groupID := newPlugin.CreateElement("groupId")
   213  	groupID.SetText("org.apache.maven.plugins")
   214  	artifactID := newPlugin.CreateElement("artifactId")
   215  	artifactID.SetText("maven-compiler-plugin")
   216  	version := newPlugin.CreateElement("version")
   217  	version.SetText("3.7.0")
   218  	configuration := newPlugin.CreateElement("configuration")
   219  	source := configuration.CreateElement("source")
   220  	source.SetText("1.8")
   221  	target := configuration.CreateElement("target")
   222  	target.SetText("1.8")
   223  	return nil
   224  }