/* * Copyright 2016 the original author or authors. * * 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 org.gradle.plugins.signing; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.gradle.api.Action; import org.gradle.api.DefaultTask; import org.gradle.api.DomainObjectSet; import org.gradle.api.InvalidUserDataException; import org.gradle.api.Task; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.PublishArtifact; import org.gradle.api.file.FileCollection; import org.gradle.api.internal.DefaultDomainObjectSet; import org.gradle.api.internal.file.FileCollectionFactory; import org.gradle.api.specs.Spec; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputFiles; import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.OutputFiles; import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.bundling.AbstractArchiveTask; import org.gradle.plugins.signing.signatory.Signatory; import org.gradle.plugins.signing.signatory.pgp.PgpKeyId; import org.gradle.plugins.signing.signatory.pgp.PgpSignatory; import org.gradle.plugins.signing.type.SignatureType; import javax.inject.Inject; import java.io.File; import java.util.Collection; import java.util.Map; import java.util.concurrent.Callable; import java.util.regex.Pattern; /** * A task for creating digital signature files for one or more; tasks, files, publishable artifacts or configurations. * * <p>The task produces {@link Signature}</p> objects that are publishable artifacts and can be assigned to another configuration. <p> The signature objects are created with defaults and using this * tasks signatory and signature type. */ public class Sign extends DefaultTask implements SignatureSpec { private static final Pattern JAVA_PARTS = Pattern.compile("[^\\p{javaJavaIdentifierPart}]"); private static final Function<Signature, File> SIGNATURE_TO_SIGN_FILE_FUNCTION = new Function<Signature, File>() { @Override public File apply(Signature input) { return input.getToSign(); } }; private static final Function<Signature, File> SIGNATURE_FILE_FUNCTION = new Function<Signature, File>() { @Override public File apply(Signature input) { return input.getFile(); } }; @Internal private SignatureType signatureType; /** * The signatory to the generated signatures. */ @Internal private Signatory signatory; private boolean required = true; private final DefaultDomainObjectSet<Signature> signatures = new DefaultDomainObjectSet<Signature>(Signature.class); public Sign() { // If we aren't required and don't have a signatory then we just don't run onlyIf(new Spec<Task>() { @Override public boolean isSatisfiedBy(Task element) { return isRequired() || getSignatory() != null; } }); getInputs().property("signatory", new Callable<String>() { @Override public String call() throws Exception { final PgpSignatory signatory = (PgpSignatory) getSignatory(); final PgpKeyId id = signatory == null ? null : signatory.getKeyId(); return id == null ? null : id.getAsHex(); } }); } @InputFiles public Iterable<File> getInputFiles() { return Iterables.transform(signatures, SIGNATURE_TO_SIGN_FILE_FUNCTION); } @OutputFiles public Map<String, File> getOutputFiles() { ArrayListMultimap<String, File> filesWithPotentialNameCollisions = ArrayListMultimap.create(); for (Signature signature : getSignatures()) { String name = JAVA_PARTS.matcher(signature.getName()).replaceAll("_"); if (name.length() > 0 && !Character.isJavaIdentifierStart(name.codePointAt(0))) { name = "_" + name.substring(1); } filesWithPotentialNameCollisions.put(name, signature.getToSign()); } Map<String, File> files = Maps.newHashMap(); for (Map.Entry<String, Collection<File>> entry : filesWithPotentialNameCollisions.asMap().entrySet()) { File[] filesWithSameName = entry.getValue().toArray(new File[0]); boolean hasMoreThanOneFileWithSameName = filesWithSameName.length > 1; for (int i = 0; i < filesWithSameName.length; i++) { File file = filesWithSameName[i]; String key = entry.getKey(); if (hasMoreThanOneFileWithSameName) { key += "$" + (i + 1); } files.put(key, file); } } return files; } /** * Configures the task to sign the archive produced for each of the given tasks (which must be archive tasks). */ public void sign(Task... tasks) { for (Task task : tasks) { if (!(task instanceof AbstractArchiveTask)) { throw new InvalidUserDataException("You cannot sign tasks that are not \'archive\' tasks, such as \'jar\', \'zip\' etc. (you tried to sign " + String.valueOf(task) + ")"); } signTask((AbstractArchiveTask) task); } } private void signTask(final AbstractArchiveTask archiveTask) { dependsOn(archiveTask); addSignature(new Signature(new Callable<File>() { public File call() { return archiveTask.getArchivePath(); } }, new Callable<String>() { public String call() { return archiveTask.getClassifier(); } }, this, this)); } /** * Configures the task to sign each of the given artifacts */ public void sign(PublishArtifact... publishArtifacts) { for (PublishArtifact publishArtifact : publishArtifacts) { signArtifact(publishArtifact); } } private void signArtifact(PublishArtifact publishArtifact) { dependsOn(publishArtifact); addSignature(new Signature(publishArtifact, this, this)); } /** * Configures the task to sign each of the given files */ public void sign(File... files) { addSignatures(null, files); } /** * Configures the task to sign each of the given artifacts, using the given classifier as the classifier for the resultant signature publish artifact. */ public void sign(String classifier, File... files) { addSignatures(classifier, files); } private void addSignatures(String classifier, File[] files) { for (File file : files) { addSignature(new Signature(file, classifier, this, this)); } } /** * Configures the task to sign every artifact of the given configurations */ public void sign(Configuration... configurations) { for (Configuration configuration : configurations) { configuration.getAllArtifacts().all( new Action<PublishArtifact>() { @Override public void execute(PublishArtifact artifact) { if (artifact instanceof Signature) { return; } signArtifact(artifact); } }); configuration.getAllArtifacts().whenObjectRemoved(new Action<PublishArtifact>() { @Override public void execute(final PublishArtifact publishArtifact) { signatures.remove(Iterables.find(signatures, new Predicate<Signature>() { @Override public boolean apply(Signature input) { return input.getToSignArtifact().equals(publishArtifact); } })); } }); } } private boolean addSignature(Signature signature) { return signatures.add(signature); } /** * Changes the signatory of the signatures. */ public void signatory(Signatory signatory) { this.signatory = signatory; } /** * Change whether or not this task should fail if no signatory or signature type are configured at the time of generation. */ public void required(boolean required) { setRequired(required); } /** * Generates the signature files. */ @TaskAction public void generate() { if (getSignatory() == null) { throw new InvalidUserDataException("Cannot perform signing task \'" + getPath() + "\' because it has no configured signatory"); } for (Signature signature : signatures) { signature.generate(); } } /** * The signatures generated by this task. */ @Internal public DomainObjectSet<Signature> getSignatures() { return signatures; } /** * Returns the single signature generated by this task. * * @return The signature. * @throws IllegalStateException if there is not exactly one signature. */ @Internal public Signature getSingleSignature() { final DomainObjectSet<Signature> signatureSet = getSignatures(); if (signatureSet.size() == 0) { throw new IllegalStateException("Expected %s to contain exactly one signature, however, it contains no signatures."); } else if (signatureSet.size() == 1) { return signatureSet.iterator().next(); } else { throw new IllegalStateException("Expected %s to contain exactly one signature, however, it contains no " + String.valueOf(signatureSet.size()) + " signatures."); } } @Inject protected FileCollectionFactory getFileCollectionFactory() { // Implementation provided by decoration throw new UnsupportedOperationException(); } /** * All of the files that will be signed by this task. */ @Internal public FileCollection getFilesToSign() { return getFileCollectionFactory().fixed("Task \'" + getPath() + "\' files to sign", Lists.newLinkedList(Iterables.filter(getInputFiles(), Predicates.notNull()))); } /** * All of the signature files that will be generated by this operation. */ @Internal public FileCollection getSignatureFiles() { return getFileCollectionFactory().fixed("Task \'" + getPath() + "\' signature files", Lists.newLinkedList(Iterables.filter(Iterables.transform(signatures, SIGNATURE_FILE_FUNCTION), Predicates.notNull()))); } public SignatureType getSignatureType() { return signatureType; } public void setSignatureType(SignatureType signatureType) { this.signatureType = signatureType; } /** * Returns the signatory for this signing task. * @return the signatory */ public Signatory getSignatory() { return signatory; } public void setSignatory(Signatory signatory) { this.signatory = signatory; } /** * Whether or not this task should fail if no signatory or signature type are configured at generation time. * * <p>Defaults to {@code true}.</p> */ @Input public boolean isRequired() { return required; } public void setRequired(boolean required) { this.required = required; } }