/* * Copyright 2015-present Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package com.facebook.buck.maven; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.jvm.java.MavenPublishable; import com.facebook.buck.log.Logger; import com.facebook.buck.model.UnflavoredBuildTarget; import com.facebook.buck.rules.BuildRule; import com.facebook.buck.rules.SourcePathResolver; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.base.StandardSystemProperty; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Multimap; import com.google.common.io.Files; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Optional; import org.apache.maven.repository.internal.MavenRepositorySystemUtils; import org.eclipse.aether.DefaultRepositorySystemSession; import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.artifact.DefaultArtifact; import org.eclipse.aether.deployment.DeployRequest; import org.eclipse.aether.deployment.DeployResult; import org.eclipse.aether.deployment.DeploymentException; import org.eclipse.aether.repository.LocalRepository; import org.eclipse.aether.repository.RemoteRepository; import org.eclipse.aether.spi.locator.ServiceLocator; import org.eclipse.aether.util.artifact.SubArtifact; public class Publisher { public static final String MAVEN_CENTRAL_URL = "https://repo1.maven.org/maven2"; private static final URL MAVEN_CENTRAL; static { try { MAVEN_CENTRAL = new URL(MAVEN_CENTRAL_URL); } catch (MalformedURLException e) { throw new RuntimeException(e); } } private static final Logger LOG = Logger.get(Publisher.class); private final ServiceLocator locator; private final LocalRepository localRepo; private final RemoteRepository remoteRepo; private final boolean dryRun; public Publisher( ProjectFilesystem repositoryFilesystem, Optional<URL> remoteRepoUrl, Optional<String> username, Optional<String> password, boolean dryRun) { this(repositoryFilesystem.getRootPath(), remoteRepoUrl, username, password, dryRun); } /** * @param localRepoPath Typically obtained as {@link * com.facebook.buck.io.ProjectFilesystem#getRootPath} * @param remoteRepoUrl Canonically {@link #MAVEN_CENTRAL_URL} * @param dryRun if true, a dummy {@link DeployResult} will be returned, with the fully * constructed {@link DeployRequest}. No actual publishing will happen */ public Publisher( Path localRepoPath, Optional<URL> remoteRepoUrl, Optional<String> username, Optional<String> password, boolean dryRun) { this.localRepo = new LocalRepository(localRepoPath.toFile()); this.remoteRepo = AetherUtil.toRemoteRepository(remoteRepoUrl.orElse(MAVEN_CENTRAL), username, password); this.locator = AetherUtil.initServiceLocator(); this.dryRun = dryRun; } public ImmutableSet<DeployResult> publish( SourcePathResolver pathResolver, ImmutableSet<MavenPublishable> publishables) throws DeploymentException { ImmutableListMultimap<UnflavoredBuildTarget, UnflavoredBuildTarget> duplicateBuiltinBuileRules = checkForDuplicatePackagedDeps(publishables); if (duplicateBuiltinBuileRules.size() > 0) { StringBuilder sb = new StringBuilder(); sb.append("Duplicate transitive dependencies for publishable libraries found! This means"); sb.append(StandardSystemProperty.LINE_SEPARATOR); sb.append("that the following libraries would have multiple copies if these libraries were"); sb.append(StandardSystemProperty.LINE_SEPARATOR); sb.append("used together. The can be resolved by adding a maven URL to each target listed"); sb.append(StandardSystemProperty.LINE_SEPARATOR); sb.append("below:"); for (UnflavoredBuildTarget unflavoredBuildTarget : duplicateBuiltinBuileRules.keySet()) { sb.append(StandardSystemProperty.LINE_SEPARATOR); sb.append(unflavoredBuildTarget.getFullyQualifiedName()); sb.append(" (referenced by these build targets: "); Joiner.on(", ").appendTo(sb, duplicateBuiltinBuileRules.get(unflavoredBuildTarget)); sb.append(")"); } throw new DeploymentException(sb.toString()); } ImmutableSet.Builder<DeployResult> deployResultBuilder = ImmutableSet.builder(); for (MavenPublishable publishable : publishables) { DefaultArtifact coords = new DefaultArtifact( Preconditions.checkNotNull( publishable.getMavenCoords().get(), "No maven coordinates specified for published rule ", publishable)); Path relativePathToOutput = pathResolver.getRelativePath( Preconditions.checkNotNull( publishable.getSourcePathToOutput(), "No path to output present in ", publishable)); File mainItem = publishable.getProjectFilesystem().resolve(relativePathToOutput).toFile(); if (!coords.getClassifier().isEmpty()) { deployResultBuilder.add(publish(coords, ImmutableList.of(mainItem))); } try { // If this is the "main" artifact (denoted by lack of classifier) generate and publish // pom alongside File pom = Pom.generatePomFile(pathResolver, publishable).toFile(); deployResultBuilder.add(publish(coords, ImmutableList.of(mainItem, pom))); } catch (IOException e) { throw new RuntimeException(e); } } return deployResultBuilder.build(); } /** * Checks for any packaged dependencies that exist between more than one of the targets that we * are trying to publish. * * @return A multimap of dependency build targets and the publishable build targets that have them * included in the final package that will be uploaded. */ private ImmutableListMultimap<UnflavoredBuildTarget, UnflavoredBuildTarget> checkForDuplicatePackagedDeps(ImmutableSet<MavenPublishable> publishables) { // First build the multimap of the builtin dependencies and the publishable targets that use // them. Multimap<UnflavoredBuildTarget, UnflavoredBuildTarget> builtinDeps = HashMultimap.create(); for (MavenPublishable publishable : publishables) { for (BuildRule buildRule : publishable.getPackagedDependencies()) { builtinDeps.put( buildRule.getBuildTarget().getUnflavoredBuildTarget(), publishable.getBuildTarget().getUnflavoredBuildTarget()); } } // Now, check for any duplicate uses, and if found, return them. ImmutableListMultimap.Builder<UnflavoredBuildTarget, UnflavoredBuildTarget> builder = ImmutableListMultimap.builder(); for (UnflavoredBuildTarget buildTarget : builtinDeps.keySet()) { Collection<UnflavoredBuildTarget> publishablesUsingBuildTarget = builtinDeps.get(buildTarget); if (publishablesUsingBuildTarget.size() > 1) { builder.putAll(buildTarget, publishablesUsingBuildTarget); } } return builder.build(); } public DeployResult publish( String groupId, String artifactId, String version, List<File> toPublish) throws DeploymentException { return publish(new DefaultArtifact(groupId, artifactId, "", version), toPublish); } /** * @param descriptor an {@link Artifact}, holding the maven coordinates for the published files * less the extension that is to be derived from the files. The {@code descriptor} itself will * not be published as is, and the {@link File} attached to it (if any) will be ignored. * @param toPublish {@link File}(s) to be published using the given coordinates. The filename * extension of each given file will be used as a maven "extension" coordinate */ public DeployResult publish(Artifact descriptor, List<File> toPublish) throws DeploymentException { String providedExtension = descriptor.getExtension(); if (!providedExtension.isEmpty()) { LOG.warn( "Provided extension %s of artifact %s to be published will be ignored. The extensions " + "of the provided file(s) will be used", providedExtension, descriptor); } List<Artifact> artifacts = new ArrayList<>(toPublish.size()); for (File file : toPublish) { artifacts.add( new SubArtifact( descriptor, descriptor.getClassifier(), Files.getFileExtension(file.getAbsolutePath()), file)); } return publish(artifacts); } /** * @param toPublish each {@link Artifact} must contain a file, that will be published under maven * coordinates in the corresponding {@link Artifact}. * @see Artifact#setFile */ public DeployResult publish(List<Artifact> toPublish) throws DeploymentException { RepositorySystem repoSys = Preconditions.checkNotNull(locator.getService(RepositorySystem.class)); DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession(); session.setLocalRepositoryManager(repoSys.newLocalRepositoryManager(session, localRepo)); session.setReadOnly(); DeployRequest deployRequest = createDeployRequest(toPublish); if (dryRun) { return new DeployResult(deployRequest) .setArtifacts(toPublish) .setMetadata(deployRequest.getMetadata()); } else { return repoSys.deploy(session, deployRequest); } } private DeployRequest createDeployRequest(List<Artifact> toPublish) { DeployRequest deployRequest = new DeployRequest().setRepository(remoteRepo); for (Artifact artifact : toPublish) { File file = artifact.getFile(); Preconditions.checkNotNull(file); Preconditions.checkArgument(file.exists(), "No such file: %s", file.getAbsolutePath()); deployRequest.addArtifact(artifact); } return deployRequest; } }