package com.prezi.spaghetti.gradle;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.prezi.spaghetti.gradle.internal.AbstractBundleModuleTask;
import com.prezi.spaghetti.gradle.internal.DefaultSpaghettiGeneratedSourceSet;
import com.prezi.spaghetti.gradle.internal.DefaultSpaghettiModuleData;
import com.prezi.spaghetti.gradle.internal.DefaultSpaghettiResourceSet;
import com.prezi.spaghetti.gradle.internal.DefaultSpaghettiSourceSet;
import com.prezi.spaghetti.gradle.internal.DefinitionAwareSpaghettiTask;
import com.prezi.spaghetti.gradle.internal.SpaghettiExtension;
import com.prezi.spaghetti.gradle.internal.SpaghettiModule;
import com.prezi.spaghetti.gradle.internal.SpaghettiModuleData;
import com.prezi.spaghetti.gradle.internal.SpaghettiModuleFactory;
import com.prezi.spaghetti.gradle.internal.SpaghettiModuleNamingScheme;
import com.prezi.spaghetti.gradle.internal.SpaghettiSourceSet;
import com.prezi.spaghetti.gradle.internal.VerifyDtsTask;
import com.prezi.spaghetti.gradle.internal.incubating.BinaryNamingScheme;
import com.prezi.spaghetti.gradle.internal.incubating.FunctionalSourceSet;
import com.prezi.spaghetti.gradle.internal.incubating.LanguageSourceSet;
import groovy.lang.Closure;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.IOFileFilter;
import org.apache.commons.io.filefilter.SuffixFileFilter;
import org.apache.commons.io.filefilter.TrueFileFilter;
import org.gradle.api.Action;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.internal.artifacts.publish.ArchivePublishArtifact;
import org.gradle.api.internal.file.FileResolver;
import org.gradle.api.tasks.bundling.Zip;
import org.gradle.internal.reflect.Instantiator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.regex.Pattern;
public class SpaghettiPlugin implements Plugin<Project> {
private static final Logger logger = LoggerFactory.getLogger(SpaghettiPlugin.class);
public static final String SPAGHETTI_GENERATED_SOURCE_SET = "spaghetti-generated";
private static final String[] MODULE_SUFFIXES = { ".module", ".module.d.ts", ".module.ts" };
private static final IOFileFilter MODULE_FILE_FILTER = new SuffixFileFilter(MODULE_SUFFIXES);
private final Instantiator instantiator;
private final FileResolver fileResolver;
@Inject
public SpaghettiPlugin(Instantiator instantiator, FileResolver fileResolver) {
this.instantiator = instantiator;
this.fileResolver = fileResolver;
}
@Override
public void apply(final Project project) {
project.getPlugins().apply(SpaghettiBasePlugin.class);
final SpaghettiExtension extension = project.getExtensions().getByType(SpaghettiExtension.class);
// Add source sets
FunctionalSourceSet mainSources = extension.getSources().maybeCreate("main");
FunctionalSourceSet testSources = extension.getSources().maybeCreate("test");
DefaultSpaghettiSourceSet spaghettiSourceSet = instantiator.newInstance(DefaultSpaghettiSourceSet.class, "spaghetti", mainSources, fileResolver);
spaghettiSourceSet.getSource().srcDir("src/main/spaghetti");
mainSources.add(spaghettiSourceSet);
DefaultSpaghettiResourceSet spaghettiResourceSet = instantiator.newInstance(DefaultSpaghettiResourceSet.class, "spaghetti-resources", mainSources, fileResolver);
spaghettiResourceSet.getSource().srcDir("src/main/spaghetti-resources");
mainSources.add(spaghettiResourceSet);
// TODO Use a proper Spaghetti module binary to tie this together
final ProcessSpaghettiResources resourcesTask = project.getTasks().create("processSpaghettiResources", ProcessSpaghettiResources.class);
resourcesTask.setDescription("Processes Spaghetti resources");
resourcesTask.getConventionMapping().map("destinationDir", new Callable<File>() {
@Override
public File call() throws Exception {
return project.file(String.valueOf(project.getBuildDir()) + "/spaghetti/resources");
}
});
resourcesTask.dependsOn(spaghettiResourceSet);
resourcesTask.from(spaghettiResourceSet.getSource());
project.getTasks().withType(DefinitionAwareSpaghettiTask.class).all(new Action<DefinitionAwareSpaghettiTask>() {
private Callable<File> callable = new Callable<File>() {
private File definition = null;
@Override
public File call() throws Exception {
if (this.definition == null) {
this.definition = findDefinition(project);
}
return this.definition;
}
};
@Override
public void execute(DefinitionAwareSpaghettiTask task) {
task.getConventionMapping().map("definition", callable);
}
});
project.getTasks().withType(AbstractBundleModuleTask.class).all(new Action<AbstractBundleModuleTask>() {
@Override
public void execute(AbstractBundleModuleTask task) {
task.getConventionMapping().map("sourceBaseUrl", new Callable<String>() {
@Override
public String call() throws Exception {
return extension.getSourceBaseUrl();
}
});
task.getConventionMapping().map("resourcesDirectoryInternal", new Callable<File>() {
@Override
public File call() throws Exception {
return resourcesTask.getDestinationDir();
}
});
task.dependsOn(resourcesTask);
}
});
// Automatically generate module headers
GenerateHeaders generateHeaders = addGenerateHeadersTask(project, "generateHeaders", "generated-headers", mainSources);
generateHeaders.setDescription("Generates Spaghetti headers.");
// Automatically generate test headers
GenerateHeaders generateTestHeaders = addGenerateHeadersTask(project, "generateTestHeaders", "generated-test-headers", testSources);
generateTestHeaders.setDescription("Generates Spaghetti test headers.");
SpaghettiBasePlugin.withDefaultTestConfiguration(project, generateTestHeaders);
// Add task for generating stubs
addGenerateStubsTask(project, testSources);
}
private GenerateHeaders addGenerateHeadersTask(final Project project, String name, final String directoryName, FunctionalSourceSet functionalSourceSet) {
final GenerateHeaders generateHeadersTask = project.getTasks().create(name, GenerateHeaders.class);
generateHeadersTask.getConventionMapping().map("outputDirectory", new Callable<File>() {
@Override
public File call() throws Exception {
return new File(project.getBuildDir(), "spaghetti/" + directoryName);
}
});
logger.debug("Created {}", generateHeadersTask);
// Create source set
LanguageSourceSet spaghettiHeaders = functionalSourceSet.findByName(SPAGHETTI_GENERATED_SOURCE_SET);
if (spaghettiHeaders == null) {
spaghettiHeaders = instantiator.newInstance(DefaultSpaghettiGeneratedSourceSet.class, SPAGHETTI_GENERATED_SOURCE_SET, functionalSourceSet, fileResolver);
functionalSourceSet.add(spaghettiHeaders);
logger.debug("Added {}", spaghettiHeaders);
}
spaghettiHeaders.getSource().srcDir(new Callable<File>() {
@Override
public File call() throws Exception {
return generateHeadersTask.getOutputDirectory();
}
});
spaghettiHeaders.builtBy(generateHeadersTask);
return generateHeadersTask;
}
private void addGenerateStubsTask(final Project project, FunctionalSourceSet functionalSourceSet) {
final GenerateStubs generateStubsTask = project.getTasks().create("generateStubs", GenerateStubs.class);
generateStubsTask.setDescription("Generates Spaghetti stubs.");
logger.debug("Created {}", generateStubsTask);
SpaghettiBasePlugin.withDefaultTestConfiguration(project, generateStubsTask);
// Create source set
LanguageSourceSet spaghettiStubs = functionalSourceSet.findByName(SPAGHETTI_GENERATED_SOURCE_SET);
if (spaghettiStubs == null) {
spaghettiStubs = instantiator.newInstance(DefaultSpaghettiGeneratedSourceSet.class, SPAGHETTI_GENERATED_SOURCE_SET, functionalSourceSet, fileResolver);
functionalSourceSet.add(spaghettiStubs);
logger.debug("Added {}", spaghettiStubs);
}
spaghettiStubs.getSource().srcDir(new Callable<File>() {
@Override
public File call() throws Exception {
return generateStubsTask.getOutputDirectory();
}
});
spaghettiStubs.builtBy(generateStubsTask);
}
public static <T> void registerSpaghettiModuleBinary(Project project, String moduleName, Callable<File> javaScriptFile, Callable<File> sourceMapFile, Callable<File> definitionOverride, Collection<?> dependencies, T payload, SpaghettiModuleFactory<T> callback) {
SpaghettiExtension spaghettiExtension = project.getExtensions().getByType(SpaghettiExtension.class);
BinaryNamingScheme namingScheme = new SpaghettiModuleNamingScheme(moduleName);
// Verify .d.ts module definition
String verifyTaskName = namingScheme.getTaskName("verifyDtsFor");
VerifyDtsTask verifyDtsTask = project.getTasks().create(verifyTaskName, VerifyDtsTask.class);
// Bundle module
BundleModule bundleTask = createBundleTask(project, namingScheme, javaScriptFile, sourceMapFile, definitionOverride, dependencies, verifyDtsTask);
Zip zipModule = createZipTask(project, namingScheme, bundleTask, namingScheme.getLifecycleTaskName(), "");
logger.debug("Added bundle task {} with zip task {}", bundleTask, zipModule);
// Obfuscate bundle
ObfuscateModule obfuscateTask = createObfuscateTask(project, namingScheme, javaScriptFile, sourceMapFile, definitionOverride, dependencies, verifyDtsTask);
Zip zipObfuscated = createZipTask(project, namingScheme, obfuscateTask, namingScheme.getLifecycleTaskName() + "-obfuscated", "obfuscated");
logger.debug("Added obfuscate task {} with zip artifact {}", obfuscateTask, zipObfuscated);
SpaghettiModuleData data = new DefaultSpaghettiModuleData(javaScriptFile, sourceMapFile, bundleTask, obfuscateTask, zipModule, zipObfuscated);
SpaghettiModule moduleBinary = callback.create(namingScheme, data, payload);
if (dependencies != null && !dependencies.isEmpty()) {
moduleBinary.builtBy(dependencies);
}
if (!moduleBinary.isUsedForTesting()) {
addBundleArtifact(project, spaghettiExtension.getConfiguration(), zipModule, "");
addBundleArtifact(project, spaghettiExtension.getObfuscatedConfiguration(), zipObfuscated, "obfuscated");
} else {
if (spaghettiExtension.getPublishTestArtifacts()) {
addBundleArtifact(project, spaghettiExtension.getTestConfiguration(), zipModule, "test");
addBundleArtifact(project, spaghettiExtension.getTestObfuscatedConfiguration(), zipObfuscated, "test-obfuscated");
}
SpaghettiBasePlugin.withDefaultTestConfiguration(project, bundleTask);
}
spaghettiExtension.getBinaries().add(moduleBinary);
}
private static void addBundleArtifact(Project project, Configuration configuration, Zip task, final String name) {
project.getArtifacts().add(configuration.getName(), task, new Closure(project) {
@Override
public Object call(Object... arguments) {
ArchivePublishArtifact artifact = (ArchivePublishArtifact) this.getDelegate();
artifact.setClassifier(name);
return artifact;
}
});
}
private static BundleModule createBundleTask(final Project project, final BinaryNamingScheme namingScheme, Callable<File> javaScriptFile, Callable<File> sourceMapFile, Callable<File> definitionOverride, Collection<?> dependencies, Task verifyDtsTask) {
String bundleTaskName = namingScheme.getTaskName("bundle");
BundleModule bundleTask = project.getTasks().create(bundleTaskName, BundleModule.class);
bundleTask.setDescription("Bundles " + namingScheme.getDescription() + " module.");
configureBundleTask(project, bundleTask, namingScheme, javaScriptFile, sourceMapFile, definitionOverride, dependencies, verifyDtsTask, "bundled");
return bundleTask;
}
private static ObfuscateModule createObfuscateTask(final Project project, final BinaryNamingScheme namingScheme, Callable<File> javaScriptFile, Callable<File> sourceMapFile, Callable<File> definitionOverride, Collection<?> dependencies, Task verifyDtsTask) {
String obfuscateTaskName = namingScheme.getTaskName("obfuscate");
ObfuscateModule obfuscateTask = project.getTasks().create(obfuscateTaskName, ObfuscateModule.class);
obfuscateTask.setDescription("Obfuscates " + namingScheme.getDescription() + " module.");
configureBundleTask(project, obfuscateTask, namingScheme, javaScriptFile, sourceMapFile, definitionOverride, dependencies, verifyDtsTask, "obfuscated");
return obfuscateTask;
}
private static void configureBundleTask(final Project project, AbstractBundleModuleTask task, final BinaryNamingScheme namingScheme, Callable<File> javaScriptFile, Callable<File> sourceMapFile, Callable<File> definitionOverride, Collection<?> dependencies, Task verifyDtsTask, final String outputDir) {
task.getConventionMapping().map("inputFile", javaScriptFile);
if (sourceMapFile != null) {
task.getConventionMapping().map("sourceMap", sourceMapFile);
}
if (definitionOverride != null) {
task.getConventionMapping().map("definitionOverride", definitionOverride);
}
task.getConventionMapping().map("outputDirectory", new Callable<File>() {
@Override
public File call() throws Exception {
return project.file(project.getBuildDir() + "/spaghetti/" + namingScheme.getOutputDirectoryBase() + "/" + outputDir);
}
});
if (dependencies != null && !dependencies.isEmpty()) {
task.dependsOn(dependencies);
}
task.dependsOn(verifyDtsTask);
}
private static Zip createZipTask(Project project, BinaryNamingScheme namingScheme, final AbstractBundleModuleTask bundleTask, final String name, String taskName) {
String zipTaskName = namingScheme.getTaskName("zip", taskName);
Zip zipTask = project.getTasks().create(zipTaskName, Zip.class);
zipTask.setDescription("Zip up " + name + " " + namingScheme.getDescription() + ".");
zipTask.dependsOn(bundleTask);
zipTask.from(new Callable<File>() {
@Override
public File call() throws Exception {
return bundleTask.getOutputDirectory();
}
});
zipTask.getConventionMapping().map("baseName", new Callable<String>() {
@Override
public String call() throws Exception {
return name;
}
});
return zipTask;
}
private static File findDefinition(Project project) {
SpaghettiExtension extension = project.getExtensions().getByType(SpaghettiExtension.class);
Set<SpaghettiSourceSet> sources = extension.getSources().getByName("main").withType(SpaghettiSourceSet.class);
List<Iterable<File>> sourceDirs = new ArrayList<Iterable<File>>();
for (SpaghettiSourceSet sourceSet : sources) {
sourceDirs.add(sourceSet.getSource().getSrcDirs());
}
sourceDirs.addAll(extension.getDefinitionSearchSourceDirs());
Set<File> definitions = Sets.newLinkedHashSet();
for (File sourceDir : Iterables.concat(sourceDirs)) {
if (sourceDir.isDirectory()) {
Collection<File> files = FileUtils.listFiles(sourceDir, MODULE_FILE_FILTER, TrueFileFilter.TRUE);
for (File file : files) {
if (!file.getName().startsWith(".")) {
definitions.add(file);
}
}
}
}
if (definitions.isEmpty()) {
return null;
} else if (definitions.size() == 1) {
return Iterables.getOnlyElement(definitions);
} else {
throw new IllegalStateException("More than one definition found: " + definitions);
}
}
}