diff --git a/build.gradle b/build.gradle index 86a42ea2..24c09bd3 100644 --- a/build.gradle +++ b/build.gradle @@ -83,6 +83,7 @@ dependencies { implementation "com.bmuschko:gradle-nexus-plugin:2.3.1" implementation "org.grails:grails-bootstrap:$grailsVersion" implementation "org.grails:grails-gradle-model:$grailsVersion" + implementation "org.grails:grails-shell:$grailsVersion" implementation "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion" implementation "io.spring.gradle:dependency-management-plugin:1.1.4" } @@ -113,6 +114,12 @@ gradlePlugin { id = 'org.grails.grails-plugin' implementationClass = 'org.grails.gradle.plugin.core.GrailsPluginGradlePlugin' } + grailsProfile { + displayName = "Grails Profile Gradle Plugin" + description = 'A plugin that is capable of compiling a Grails profile into a JAR file for distribution' + id = 'org.grails.grails-profile' + implementationClass = 'org.grails.gradle.plugin.profiles.GrailsProfileGradlePlugin' + } grailsWeb { displayName = "Grails Web Gradle Plugin" description = 'Adds web specific extensions' @@ -125,6 +132,12 @@ gradlePlugin { id = 'org.grails.internal.grails-plugin-publish' implementationClass = 'org.grails.gradle.plugin.publishing.internal.GrailsCentralPublishGradlePlugin' } + grailsProfilePublish { + displayName = "Grails Profile Publish Plugin" + description = 'A plugin for publishing profiles' + id = 'org.grails.internal.grails-profile-publish' + implementationClass = 'org.grails.gradle.plugin.profiles.internal.GrailsProfilePublishGradlePlugin' + } } } diff --git a/src/main/groovy/org/grails/gradle/plugin/profiles/GrailsProfileGradlePlugin.groovy b/src/main/groovy/org/grails/gradle/plugin/profiles/GrailsProfileGradlePlugin.groovy new file mode 100644 index 00000000..570a9bb0 --- /dev/null +++ b/src/main/groovy/org/grails/gradle/plugin/profiles/GrailsProfileGradlePlugin.groovy @@ -0,0 +1,170 @@ +/* + * Copyright 2015 original 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.grails.gradle.plugin.profiles + +import grails.io.IOUtils +import grails.util.BuildSettings +import groovy.transform.CompileStatic +import org.apache.tools.ant.DirectoryScanner +import org.gradle.api.Action +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.DependencyResolveDetails +import org.gradle.api.file.CopySpec +import org.gradle.api.internal.artifacts.publish.ArchivePublishArtifact +import org.gradle.api.plugins.BasePlugin +import org.gradle.api.tasks.Copy +import org.gradle.api.tasks.bundling.Jar +import org.grails.cli.profile.commands.script.GroovyScriptCommand +import org.grails.gradle.plugin.profiles.tasks.ProfileCompilerTask + + +import static org.gradle.api.plugins.BasePlugin.* + +/** + * A plugin that is capable of compiling a Grails profile into a JAR file for distribution + * + * @author Graeme Rocher + * @since 3.1 + */ +@CompileStatic +class GrailsProfileGradlePlugin implements Plugin { + + static final String CONFIGURATION_NAME = 'grails' + + public static final String RUNTIME_CONFIGURATION = "profileRuntimeOnly" + + @Override + void apply(Project project) { + project.getPluginManager().apply(BasePlugin.class) + project.configurations.create(CONFIGURATION_NAME) + def profileConfiguration = project.configurations.create(RUNTIME_CONFIGURATION) + + profileConfiguration.resolutionStrategy.eachDependency { + DependencyResolveDetails details = (DependencyResolveDetails)it + def requested = details.requested + def group = requested.group + def version = requested.version + + if(!group || !version) { + group = group ?: "org.grails.profiles" + version = version ?: BuildSettings.grailsVersion + + details.useTarget(group: group, name: requested.name,version:version) + } + } + + def profileYml = project.file("profile.yml") + + def commandsDir = project.file("commands") + def resourcesDir = new File(project.buildDir, "resources/profile") + def templatesDir = project.file("templates") + def skeletonsDir = project.file("skeleton") + def featuresDir = project.file("features") + + def spec1 = project.copySpec { CopySpec spec -> + spec.from(commandsDir) + spec.exclude("*.groovy") + spec.into("commands") + } + def spec2 = project.copySpec { CopySpec spec -> + spec.from(templatesDir) + spec.into("templates") + } + def spec4 = project.copySpec { CopySpec spec -> + spec.from(featuresDir) + spec.into("features") + } + def spec3 = project.copySpec { CopySpec spec -> + spec.from(skeletonsDir) + spec.into("skeleton") + } + + def processResources = project.tasks.create("processResources", Copy, (Action){ Copy c -> + c.with(spec1, spec2, spec3, spec4) + c.into(new File(resourcesDir, "/META-INF/grails-profile")) + + c.doFirst { + for(String file in DirectoryScanner.defaultExcludes) { + DirectoryScanner.removeDefaultExclude(file) + } + } + c.doLast { + DirectoryScanner.resetDefaultExcludes() + } + }) + + def classsesDir = new File(project.buildDir, "classes/profile") + def compileTask = project.tasks.create("compileProfile", ProfileCompilerTask, (Action) { ProfileCompilerTask task -> + task.destinationDir = classsesDir + task.source = commandsDir + task.config = profileYml + if(templatesDir.exists()) { + task.templatesDir = templatesDir + } + task.classpath = project.configurations.getByName(RUNTIME_CONFIGURATION) + project.files(IOUtils.findJarFile(GroovyScriptCommand)) + }) + + def jarTask = project.tasks.create("jar", Jar, (Action) { Jar jar -> + jar.dependsOn(processResources, compileTask) + jar.from(resourcesDir) + jar.from(classsesDir) + jar.destinationDir = new File(project.buildDir, "libs") + jar.setDescription("Assembles a jar archive containing the profile classes.") + jar.setGroup(BUILD_GROUP) + + ArchivePublishArtifact jarArtifact = new ArchivePublishArtifact(jar) + project.artifacts.add(CONFIGURATION_NAME, jarArtifact) + + jar.doFirst { + for(String file in DirectoryScanner.defaultExcludes) { + DirectoryScanner.removeDefaultExclude(file) + } + } + jar.doLast { + DirectoryScanner.resetDefaultExcludes() + } + }) + + project.tasks.create("sourcesJar", Jar, (Action) { Jar jar -> + jar.from(commandsDir) + if(profileYml.exists()) { + jar.from(profileYml) + } + jar.from(templatesDir) { CopySpec spec -> + spec.into("templates") + } + jar.from(skeletonsDir) { CopySpec spec -> + spec.into("skeleton") + } + jar.archiveClassifier.set("sources") + jar.destinationDirectory.set(new File(project.buildDir, "libs")) + jar.setDescription("Assembles a jar archive containing the profile sources.") + jar.setGroup(BUILD_GROUP) + + jar.doFirst { + for(String file in DirectoryScanner.defaultExcludes) { + DirectoryScanner.removeDefaultExclude(file) + } + } + jar.doLast { + DirectoryScanner.resetDefaultExcludes() + } + }) + project.tasks.findByName("assemble").dependsOn jarTask + + } +} diff --git a/src/main/groovy/org/grails/gradle/plugin/profiles/internal/GrailsProfilePublishGradlePlugin.groovy b/src/main/groovy/org/grails/gradle/plugin/profiles/internal/GrailsProfilePublishGradlePlugin.groovy new file mode 100644 index 00000000..9e7389b2 --- /dev/null +++ b/src/main/groovy/org/grails/gradle/plugin/profiles/internal/GrailsProfilePublishGradlePlugin.groovy @@ -0,0 +1,112 @@ +/* + * Copyright 2015 original 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.grails.gradle.plugin.profiles.internal + + +import groovy.transform.CompileStatic +import org.gradle.api.Action +import org.gradle.api.Project +import org.gradle.api.XmlProvider +import org.gradle.api.artifacts.Dependency +import org.gradle.api.artifacts.DependencySet +import org.gradle.api.artifacts.SelfResolvingDependency +import org.gradle.api.publish.maven.MavenPublication +import org.gradle.api.tasks.bundling.Jar +import org.grails.gradle.plugin.profiles.GrailsProfileGradlePlugin +import org.grails.gradle.plugin.publishing.internal.GrailsCentralPublishGradlePlugin + +import java.nio.file.Files + +import static org.gradle.api.plugins.BasePlugin.BUILD_GROUP + +/** + * A plugin for publishing profiles + * + * @author Graeme Rocher + * @since 3.1 + */ +@CompileStatic +class GrailsProfilePublishGradlePlugin extends GrailsCentralPublishGradlePlugin { + + @Override + void apply(Project project) { + super.apply(project) + final File tempReadmeForJavadoc = Files.createTempFile("README", "txt").toFile() + tempReadmeForJavadoc << "https://central.sonatype.org/publish/requirements/#supply-javadoc-and-sources" + project.tasks.create("javadocJar", Jar, (Action) { Jar jar -> + jar.from(tempReadmeForJavadoc) + jar.archiveClassifier.set("javadoc") + jar.destinationDirectory.set(new File(project.buildDir, "libs")) + jar.setDescription("Assembles a jar archive containing the profile javadoc.") + jar.setGroup(BUILD_GROUP) + }) + } + + @Override + protected String getDefaultGrailsCentralReleaseRepo() { + "https://repo.grails.org/grails/libs-releases-local" + } + + @Override + protected String getDefaultGrailsCentralSnapshotRepo() { + "https://repo.grails.org/grails/libs-snapshots-local" + } + + @Override + protected Map getDefaultExtraArtifact(Project project) { + [source: "${project.buildDir}/classes/profile/META-INF/grails-profile/profile.yml".toString(), + classifier: defaultClassifier, + extension : 'yml'] + } + + @Override + protected String getDefaultClassifier() { + 'profile' + } + + @Override + protected String getDefaultRepo() { + 'profiles' + } + + @Override + protected void doAddArtefact(Project project, MavenPublication publication) { + publication.artifact(project.tasks.findByName("jar")) + publication.pom(new Action() { + @Override + void execute(org.gradle.api.publish.maven.MavenPom mavenPom) { + mavenPom.withXml(new Action() { + @Override + void execute(XmlProvider xml) { + Node dependenciesNode = xml.asNode().appendNode('dependencies') + + DependencySet dependencySet = project.configurations[GrailsProfileGradlePlugin.RUNTIME_CONFIGURATION].allDependencies + + for (Dependency dependency : dependencySet) { + if (! (dependency instanceof SelfResolvingDependency)) { + Node dependencyNode = dependenciesNode.appendNode('dependency') + dependencyNode.appendNode('groupId', dependency.group) + dependencyNode.appendNode('artifactId', dependency.name) + dependencyNode.appendNode('version', dependency.version) + dependencyNode.appendNode('scope', GrailsProfileGradlePlugin.RUNTIME_CONFIGURATION) + } + } + } + }) + } + }) + } +} diff --git a/src/main/groovy/org/grails/gradle/plugin/profiles/tasks/ProfileCompilerTask.groovy b/src/main/groovy/org/grails/gradle/plugin/profiles/tasks/ProfileCompilerTask.groovy new file mode 100644 index 00000000..7f26fe80 --- /dev/null +++ b/src/main/groovy/org/grails/gradle/plugin/profiles/tasks/ProfileCompilerTask.groovy @@ -0,0 +1,192 @@ +/* + * Copyright 2015 original 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.grails.gradle.plugin.profiles.tasks + +import groovy.transform.CompileStatic +import org.codehaus.groovy.control.CompilationUnit +import org.codehaus.groovy.control.CompilerConfiguration +import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer +import org.codehaus.groovy.control.customizers.ImportCustomizer +import org.gradle.api.artifacts.Dependency +import org.gradle.api.file.FileTree +import org.gradle.api.file.FileVisitDetails +import org.gradle.api.tasks.* +import org.gradle.api.tasks.compile.AbstractCompile +import org.grails.cli.profile.commands.script.GroovyScriptCommand +import org.grails.cli.profile.commands.script.GroovyScriptCommandTransform +import org.grails.gradle.plugin.profiles.GrailsProfileGradlePlugin +import org.yaml.snakeyaml.DumperOptions +import org.yaml.snakeyaml.LoaderOptions +import org.yaml.snakeyaml.Yaml +import org.yaml.snakeyaml.constructor.SafeConstructor +import org.yaml.snakeyaml.representer.Representer + +/** + * Compiles the classes for a profile + * + * @author Graeme Rocher + * @since 3.1 + */ +@CompileStatic +class ProfileCompilerTask extends AbstractCompile { + + public static final String DEFAULT_COMPATIBILITY = "1.8" + public static final String PROFILE_NAME = "name" + public static final String PROFILE_COMMANDS = "commands" + + ProfileCompilerTask() { + setSourceCompatibility(DEFAULT_COMPATIBILITY) + setTargetCompatibility(DEFAULT_COMPATIBILITY) + + } + + @InputFile + @Optional + File config + + @OutputFile + File profileFile + + @InputDirectory + @Optional + File templatesDir + + @Override + @InputFiles + FileTree getSource() { + return (super.getSource() + project.files(config)).asFileTree + } + + @Override + void setDestinationDir(File destinationDir) { + profileFile = new File(destinationDir, "META-INF/grails-profile/profile.yml") + super.setDestinationDir(destinationDir) + } + + @TaskAction + void execute() { + + boolean profileYmlExists = config?.exists() + + def options = new DumperOptions() + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK) + def yaml = new Yaml(new SafeConstructor(new LoaderOptions()), new Representer(options), options) + Map profileData + if (profileYmlExists) { + profileData = (Map) config.withReader { BufferedReader r -> + yaml.load(r) + } + } else { + profileData = new LinkedHashMap() + } + + profileData.put(PROFILE_NAME, project.name) + + profileFile.parentFile.mkdirs() + + + if (!profileData.containsKey("extends")) { + List dependencies = [] + project.configurations.getByName(GrailsProfileGradlePlugin.RUNTIME_CONFIGURATION).allDependencies.all() { Dependency d -> + dependencies.add("${d.group}:${d.name}:${d.version}".toString()) + } + profileData.put("extends", dependencies.join(',')) + } + + def groovySourceFiles = getSource().files.findAll() { File f -> + f.name.endsWith('.groovy') + } as File[] + def ymlSourceFiles = getSource().files.findAll() { File f -> + f.name.endsWith('.yml') && f.name != 'profile.yml' + } as File[] + + Map commandNames = [:] + for (File f in groovySourceFiles) { + def fn = f.name + commandNames.put(fn - '.groovy', fn) + } + for (File f in ymlSourceFiles) { + def fn = f.name + commandNames.put(fn - '.yml', fn) + } + + if (commandNames) { + profileData.put(PROFILE_COMMANDS, commandNames) + } + + if (profileYmlExists) { + def parentDir = config.parentFile.canonicalFile + def featureDirs = new File(parentDir, "features").listFiles({ File f -> f.isDirectory() && !f.name.startsWith('.') } as FileFilter) + if (featureDirs) { + Map map = (Map) profileData.get("features") + if (map == null) { + map = [:] + profileData.put("features", map) + } + List featureNames = [] + for (f in featureDirs) { + featureNames.add f.name + } + if (featureNames) { + map.put("provided", featureNames) + } + profileData.put("features", map) + } + } + + + List templates = [] + if (templatesDir?.exists()) { + project.fileTree(templatesDir).visit { FileVisitDetails f -> + if (!f.isDirectory() && !f.name.startsWith('.')) { + templates.add f.relativePath.pathString + } + } + } + + if (templates) { + profileData.put("templates", templates) + } + + profileFile.withWriter { BufferedWriter w -> + yaml.dump(profileData, w) + } + + if (groovySourceFiles) { + + CompilerConfiguration configuration = new CompilerConfiguration() + configuration.setScriptBaseClass(GroovyScriptCommand.name) + destinationDir.mkdirs() + configuration.setTargetDirectory(destinationDir) + + def importCustomizer = new ImportCustomizer() + importCustomizer.addStarImports("org.grails.cli.interactive.completers") + importCustomizer.addStarImports("grails.util") + importCustomizer.addStarImports("grails.codegen.model") + configuration.addCompilationCustomizers(importCustomizer, new ASTTransformationCustomizer(new GroovyScriptCommandTransform())) + + for (source in groovySourceFiles) { + + CompilationUnit compilationUnit = new CompilationUnit(configuration) + configuration.compilationCustomizers.clear() + configuration.compilationCustomizers.addAll(importCustomizer, new ASTTransformationCustomizer(new GroovyScriptCommandTransform())) + compilationUnit.addSource(source) + compilationUnit.compile() + } + } + } +}