github.com/replit/upm@v0.0.0-20240423230255-9ce4fc3ea24c/internal/backends/java/java.go (about) 1 // Package java provides a backend for Java using maven. 2 package java 3 4 import ( 5 "context" 6 "encoding/xml" 7 "fmt" 8 "os" 9 "os/exec" 10 "regexp" 11 12 "github.com/replit/upm/internal/api" 13 "github.com/replit/upm/internal/nix" 14 "github.com/replit/upm/internal/util" 15 "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" 16 ) 17 18 type Dependency struct { 19 XMLName xml.Name `xml:"dependency"` 20 GroupId string `xml:"groupId"` 21 ArtifactId string `xml:"artifactId"` 22 Version string `xml:"version"` 23 PackageType string `xml:"type"` 24 } 25 26 type DynamicDependency struct { 27 XMLName xml.Name `xml:"DynamicDependency"` 28 GroupId string `xml:"groupId"` 29 ArtifactId string `xml:"artifactId"` 30 Version string `xml:"version"` 31 Classifier string `xml:"classifier"` 32 RepositoryType string `xml:"repositoryType"` 33 } 34 35 type PluginConfiguration struct { 36 XMLName xml.Name `xml:"configuration"` 37 DynamicDependencies []DynamicDependency `xml:"dynamicDependencies>DynamicDependency"` 38 } 39 40 type Plugin struct { 41 XMLName xml.Name `xml:"plugin"` 42 GroupId string `xml:"groupId"` 43 ArtifactId string `xml:"artifactId"` 44 Version string `xml:"version"` 45 Configuration PluginConfiguration `xml:"configuration"` 46 } 47 48 type Project struct { 49 XMLName xml.Name `xml:"project"` 50 ModelVersion string `xml:"modelVersion"` 51 GroupId string `xml:"groupId"` 52 ArtifactId string `xml:"artifactId"` 53 Version string `xml:"version"` 54 Dependencies []Dependency `xml:"dependencies>dependency"` 55 Plugins []Plugin `xml:"build>plugins>plugin"` 56 } 57 58 const initialPomXml = ` 59 <project> 60 <modelVersion>4.0.0</modelVersion> 61 <groupId>mygroupid</groupId> 62 <artifactId>myartifactid</artifactId> 63 <version>0.0-SNAPSHOT</version> 64 <build> 65 <plugins> 66 <plugin> 67 <groupId>de.qaware.maven</groupId> 68 <artifactId>go-offline-maven-plugin</artifactId> 69 <version>1.2.5</version> 70 <configuration> 71 <dynamicDependencies> 72 <DynamicDependency> 73 <groupId>org.apache.maven.surefire</groupId> 74 <artifactId>surefire-junit4</artifactId> 75 <version>2.20.1</version> 76 <repositoryType>PLUGIN</repositoryType> 77 </DynamicDependency> 78 <DynamicDependency> 79 <groupId>com.querydsl</groupId> 80 <artifactId>querydsl-apt</artifactId> 81 <version>4.2.1</version> 82 <classifier>jpa</classifier> 83 <repositoryType>MAIN</repositoryType> 84 </DynamicDependency> 85 </dynamicDependencies> 86 </configuration> 87 </plugin> 88 </plugins> 89 </build> 90 </project> 91 ` 92 93 var pkgNameRegexp = regexp.MustCompile("^([^:]+):([^:]+)") // groupid:artifactid 94 95 // javaPatterns is the FilenamePatterns value for JavaBackend. 96 var javaPatterns = []string{"*.java"} 97 98 func readProjectOrMakeEmpty(path string) Project { 99 var project Project 100 var xmlbytes []byte 101 if util.Exists("pom.xml") { 102 var err error 103 xmlbytes, err = os.ReadFile("pom.xml") 104 if err != nil { 105 util.DieIO("error reading pom.xml: %s", err) 106 } 107 } else { 108 xmlbytes = []byte(initialPomXml) 109 } 110 err := xml.Unmarshal(xmlbytes, &project) 111 if err != nil { 112 util.DieProtocol("error unmarshalling pom.xml: %s", err) 113 } 114 return project 115 } 116 117 const pomdotxml = "pom.xml" 118 119 func isAvailable() bool { 120 _, err := exec.LookPath("mvn") 121 return err == nil 122 } 123 124 func addPackages(ctx context.Context, pkgs map[api.PkgName]api.PkgSpec, projectName string) { 125 //nolint:ineffassign,wastedassign,staticcheck 126 span, ctx := tracer.StartSpanFromContext(ctx, "Java add package") 127 defer span.Finish() 128 project := readProjectOrMakeEmpty(pomdotxml) 129 existingDependencies := map[api.PkgName]api.PkgVersion{} 130 for _, dependency := range project.Dependencies { 131 pkgName := api.PkgName( 132 fmt.Sprintf("%s:%s", dependency.GroupId, dependency.ArtifactId), 133 ) 134 pkgVersion := api.PkgVersion(dependency.Version) 135 existingDependencies[pkgName] = pkgVersion 136 } 137 138 newDependencies := []Dependency{} 139 for pkgName, pkgSpec := range pkgs { 140 submatches := pkgNameRegexp.FindStringSubmatch(string(pkgName)) 141 if nil == submatches { 142 util.DieConsistency( 143 "package name %s does not match groupid:artifactid pattern", 144 pkgName, 145 ) 146 } 147 148 groupId := submatches[1] 149 artifactId := submatches[2] 150 if _, ok := existingDependencies[pkgName]; ok { 151 // this package is already in the lock file 152 continue 153 } 154 155 var query string 156 if pkgSpec == "" { 157 query = fmt.Sprintf("g:%s AND a:%s", groupId, artifactId) 158 } else { 159 query = fmt.Sprintf("g:%s AND a:%s AND v:%s", groupId, artifactId, pkgSpec) 160 } 161 searchDocs, err := Search(query) 162 if err != nil { 163 util.DieNetwork( 164 "error searching maven for latest version of %s:%s: %s", 165 groupId, 166 artifactId, 167 err, 168 ) 169 } 170 if len(searchDocs) == 0 { 171 if pkgSpec == "" { 172 util.DieConsistency("did not find a package %s:%s", groupId, artifactId) 173 } else { 174 util.DieConsistency("did not find a package %s:%s:%s", groupId, artifactId, pkgSpec) 175 } 176 } 177 searchDoc := searchDocs[0] 178 179 var versionString string 180 if pkgSpec == "" { 181 versionString = searchDoc.Version 182 } else { 183 versionString = string(pkgSpec) 184 } 185 186 var packageType string 187 if searchDoc.PackageType == "pom" { 188 packageType = "pom" 189 } else { 190 packageType = "jar" 191 } 192 193 dependency := Dependency{ 194 GroupId: submatches[1], 195 ArtifactId: submatches[2], 196 Version: versionString, 197 PackageType: packageType, 198 } 199 newDependencies = append(newDependencies, dependency) 200 201 } 202 203 project.Dependencies = append(project.Dependencies, newDependencies...) 204 marshalled, err := xml.MarshalIndent(project, "", " ") 205 if err != nil { 206 util.DieProtocol("could not marshal pom: %s", err) 207 } 208 209 contentsB := []byte(marshalled) 210 util.ProgressMsg("write pom.xml") 211 util.TryWriteAtomic("pom.xml", contentsB) 212 } 213 214 func removePackages(ctx context.Context, pkgs map[api.PkgName]bool) { 215 //nolint:ineffassign,wastedassign,staticcheck 216 span, ctx := tracer.StartSpanFromContext(ctx, "Java remove package") 217 defer span.Finish() 218 project := readProjectOrMakeEmpty(pomdotxml) 219 220 dependenciesToKeep := []Dependency{} 221 for _, dependency := range project.Dependencies { 222 pkgName := api.PkgName( 223 fmt.Sprintf("%s:%s", dependency.GroupId, dependency.ArtifactId), 224 ) 225 if _, ok := pkgs[pkgName]; ok { 226 // removing this dependency 227 } else { 228 dependenciesToKeep = append(dependenciesToKeep, dependency) 229 } 230 } 231 232 projectWithFilteredDependencies := project 233 projectWithFilteredDependencies.Dependencies = dependenciesToKeep 234 235 marshalled, err := xml.MarshalIndent(projectWithFilteredDependencies, "", " ") 236 if err != nil { 237 util.DieProtocol("error marshalling pom.xml: %s", err) 238 } 239 contentsB := []byte(marshalled) 240 util.ProgressMsg("write pom.xml") 241 util.TryWriteAtomic("pom.xml", contentsB) 242 243 os.RemoveAll("target/dependency") 244 } 245 246 func listSpecfile(mergeAllGroups bool) map[api.PkgName]api.PkgSpec { 247 project := readProjectOrMakeEmpty(pomdotxml) 248 pkgs := map[api.PkgName]api.PkgSpec{} 249 for _, dependency := range project.Dependencies { 250 pkgName := api.PkgName( 251 fmt.Sprintf("%s:%s", dependency.GroupId, dependency.ArtifactId), 252 ) 253 pkgSpec := api.PkgSpec(dependency.Version) 254 pkgs[pkgName] = pkgSpec 255 } 256 return pkgs 257 } 258 259 func listLockfile() map[api.PkgName]api.PkgVersion { 260 project := readProjectOrMakeEmpty(pomdotxml) 261 pkgs := map[api.PkgName]api.PkgVersion{} 262 for _, dependency := range project.Dependencies { 263 pkgName := api.PkgName( 264 fmt.Sprintf("%s:%s", dependency.GroupId, dependency.ArtifactId), 265 ) 266 pkgVersion := api.PkgVersion(dependency.Version) 267 pkgs[pkgName] = pkgVersion 268 } 269 return pkgs 270 } 271 272 func search(query string) []api.PkgInfo { 273 searchDocs, err := Search(query) 274 if err != nil { 275 util.DieNetwork("error searching maven %s", err) 276 } 277 pkgInfos := []api.PkgInfo{} 278 for _, searchDoc := range searchDocs { 279 pkgInfo := api.PkgInfo{ 280 Name: fmt.Sprintf("%s:%s", searchDoc.Group, searchDoc.Artifact), 281 Version: searchDoc.Version, 282 } 283 pkgInfos = append(pkgInfos, pkgInfo) 284 } 285 return pkgInfos 286 } 287 288 func info(pkgName api.PkgName) api.PkgInfo { 289 searchDoc, err := Info(string(pkgName)) 290 291 if err != nil { 292 util.DieNetwork("error searching maven %s", err) 293 } 294 295 if searchDoc.Artifact == "" { 296 return api.PkgInfo{} 297 } 298 299 pkgInfo := api.PkgInfo{ 300 Name: fmt.Sprintf("%s:%s", searchDoc.Group, searchDoc.Artifact), 301 Version: searchDoc.CurrentVersion, 302 } 303 return pkgInfo 304 } 305 306 // JavaBackend is the UPM language backend for Java using Maven. 307 var JavaBackend = api.LanguageBackend{ 308 Name: "java-maven", 309 Specfile: pomdotxml, 310 Lockfile: pomdotxml, 311 IsAvailable: isAvailable, 312 FilenamePatterns: javaPatterns, 313 Quirks: api.QuirksAddRemoveAlsoLocks, 314 GetPackageDir: func() string { 315 return "target/dependency" 316 }, 317 Search: search, 318 Info: info, 319 Add: addPackages, 320 Remove: removePackages, 321 Install: func(ctx context.Context) { 322 //nolint:ineffassign,wastedassign,staticcheck 323 span, ctx := tracer.StartSpanFromContext(ctx, "Maven install") 324 defer span.Finish() 325 util.RunCmd([]string{ 326 "mvn", 327 "de.qaware.maven:go-offline-maven-plugin:resolve-dependencies", 328 "dependency:copy-dependencies", 329 }) 330 }, 331 ListSpecfile: listSpecfile, 332 ListLockfile: listLockfile, 333 Lock: func(ctx context.Context) {}, 334 InstallReplitNixSystemDependencies: nix.DefaultInstallReplitNixSystemDependencies, 335 }