diff --git a/README.md b/README.md index bbe260e..bb2039a 100644 --- a/README.md +++ b/README.md @@ -92,18 +92,23 @@ GraalVM supports creating a single Ahead-Of-Time [native executable](https://www.graalvm.org/native-image/) from your java bytecode. -Once you have at least GraalVM 22.3.1 Java 19 installed, you can generate the -UberJar as above, then create your native binary as such: +Once you have at least GraalVM 22.3.1 Java 17 installed, you can generate the native binary with: ```bash -native-image --no-fallback --enable-preview -jar app/build/libs/hello-uber-1.0.jar +./gradlew nativeCompile ``` -You'll end up with a `hello-uber-1.0` file that can be executed directly without +You'll end up with a `hello-1.0` file that can be executed directly without the need of a JVM: ```bash -./hello-uber-1.0 +./app/build/native/nativeCompile/hello-1.0 +``` + +Alternatively, you can run the native executable directly with: + +```bash +./gradlew nativeRun ``` > **NOTE:** RIFE2 support for GraalVM native-image is still in preliminary diff --git a/app/build.gradle.kts b/app/build.gradle.kts index df9f28e..0193db3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,16 +1,22 @@ import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.gradle.api.tasks.testing.logging.TestLogEvent +import com.uwyn.rife2.gradle.TemplateType.* plugins { + application id("com.uwyn.rife2") `maven-publish` + id("org.graalvm.buildtools.native") version "0.9.20" } -version = 1.0 -group = "com.example" - base { archivesName.set("hello") + version = 1.0 + group = "com.example" +} + +application { + mainClass.set("hello.App") } java { @@ -26,9 +32,9 @@ repositories { } rife2 { - mainClass.set("hello.App") - version.set("1.3.0") + version.set("1.4.0") useAgent.set(true) + precompiledTemplateTypes.add(HTML) } dependencies { @@ -63,3 +69,8 @@ publishing { } } } + +graalvmNative.binaries.all { + buildArgs.add("--enable-preview") // support for Jetty virtual threads with JDK 19 + imageName.set("hello-$version") +} diff --git a/app/src/main/java/hello/App.java b/app/src/main/java/hello/App.java index a7bed73..43a1870 100644 --- a/app/src/main/java/hello/App.java +++ b/app/src/main/java/hello/App.java @@ -13,4 +13,4 @@ public class App extends Site { .staticResourceBase("src/main/webapp") .start(new App()); } -} \ No newline at end of file +} diff --git a/app/src/main/resources/META-INF/native-image/reflect-config.json b/app/src/main/resources/META-INF/native-image/reflect-config.json index 9b0c3be..3f485d5 100644 --- a/app/src/main/resources/META-INF/native-image/reflect-config.json +++ b/app/src/main/resources/META-INF/native-image/reflect-config.json @@ -1,6 +1,6 @@ [ -{ - "name":"rife.template.html.hello", - "methods":[{"name":"","parameterTypes":[] }] -} + { + "name":"rife.template.html.hello", + "methods":[{"name":"","parameterTypes":[] }] + } ] diff --git a/app/src/main/templates/hello.html b/app/src/main/resources/templates/hello.html similarity index 96% rename from app/src/main/templates/hello.html rename to app/src/main/resources/templates/hello.html index f7ffe8b..59ff81b 100644 --- a/app/src/main/templates/hello.html +++ b/app/src/main/resources/templates/hello.html @@ -8,4 +8,4 @@

Hello World

- \ No newline at end of file + diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index b9ea647..c7fabff 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -1,5 +1,6 @@ plugins { `java-gradle-plugin` + groovy } repositories { @@ -8,13 +9,8 @@ repositories { dependencies { gradleApi() -} - -tasks { - withType { - options.isDeprecation = true - options.compilerArgs.add("-Xlint:unchecked") - } + testImplementation(libs.spock.core) + testImplementation(gradleTestKit()) } gradlePlugin { @@ -25,3 +21,11 @@ gradlePlugin { } } } + +tasks.withType().configureEach { + useJUnitPlatform() + testLogging { + exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL + events = setOf(org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED, org.gradle.api.tasks.testing.logging.TestLogEvent.SKIPPED, org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED) + } +} diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts index 7fbbd44..9ac59bd 100644 --- a/build-logic/settings.gradle.kts +++ b/build-logic/settings.gradle.kts @@ -1 +1,9 @@ rootProject.name = "build-logic" + +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} diff --git a/build-logic/src/main/java/com/uwyn/rife2/gradle/PrecompileTemplates.java b/build-logic/src/main/java/com/uwyn/rife2/gradle/PrecompileTemplates.java index 871df2e..eac8e6c 100644 --- a/build-logic/src/main/java/com/uwyn/rife2/gradle/PrecompileTemplates.java +++ b/build-logic/src/main/java/com/uwyn/rife2/gradle/PrecompileTemplates.java @@ -18,11 +18,12 @@ package com.uwyn.rife2.gradle; import org.gradle.api.DefaultTask; import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Property; import org.gradle.api.tasks.CacheableTask; import org.gradle.api.tasks.Classpath; import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.InputDirectory; +import org.gradle.api.tasks.InputFiles; import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.OutputDirectory; import org.gradle.api.tasks.PathSensitive; @@ -31,6 +32,7 @@ import org.gradle.api.tasks.TaskAction; import org.gradle.process.ExecOperations; import javax.inject.Inject; +import java.nio.file.Files; import java.util.ArrayList; import java.util.List; @@ -40,12 +42,12 @@ public abstract class PrecompileTemplates extends DefaultTask { @Classpath public abstract ConfigurableFileCollection getClasspath(); - @InputDirectory + @InputFiles @PathSensitive(PathSensitivity.RELATIVE) - public abstract DirectoryProperty getTemplatesDirectory(); + public abstract ConfigurableFileCollection getTemplatesDirectories(); @Input - public abstract Property getType(); + public abstract ListProperty getTypes(); @Input @Optional @@ -63,21 +65,28 @@ public abstract class PrecompileTemplates extends DefaultTask { @TaskAction public void precompileTemplates() { - getExecOperations().javaexec(javaexec -> { - javaexec.setClasspath(getClasspath()); - javaexec.getMainClass().set("rife.template.TemplateDeployer"); - List args = new ArrayList<>(); - if (getVerbose().isPresent() && Boolean.TRUE.equals(getVerbose().get())) { - args.add("-verbose"); - } - args.add("-t"); - args.add(getType().get()); - args.add("-d"); - args.add(getOutputDirectory().get().getAsFile().getPath()); - args.add("-encoding"); - args.add(getEncoding().orElse("UTF-8").get()); - args.add(getTemplatesDirectory().get().getAsFile().getPath()); - javaexec.args(args); - }); + for (var type : getTypes().get()) { + getTemplatesDirectories().getFiles().forEach(dir -> { + if (Files.exists(dir.toPath())) { + getExecOperations().javaexec(javaexec -> { + javaexec.setClasspath(getClasspath()); + javaexec.getMainClass().set("rife.template.TemplateDeployer"); + List args = new ArrayList<>(); + if (getVerbose().isPresent() && Boolean.TRUE.equals(getVerbose().get())) { + args.add("-verbose"); + } + args.add("-t"); + args.add(type.identifier()); + args.add("-d"); + args.add(getOutputDirectory().get().getAsFile().getPath()); + args.add("-encoding"); + args.add(getEncoding().orElse("UTF-8").get()); + args.add(dir.getPath()); + javaexec.args(args); + }); + } + }); + + } } } diff --git a/build-logic/src/main/java/com/uwyn/rife2/gradle/Rife2Extension.java b/build-logic/src/main/java/com/uwyn/rife2/gradle/Rife2Extension.java index ebd97e0..c4c641c 100644 --- a/build-logic/src/main/java/com/uwyn/rife2/gradle/Rife2Extension.java +++ b/build-logic/src/main/java/com/uwyn/rife2/gradle/Rife2Extension.java @@ -15,15 +15,18 @@ */ package com.uwyn.rife2.gradle; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Property; -@SuppressWarnings("unused") public abstract class Rife2Extension { - public abstract Property getMainClass(); - public abstract Property getVersion(); public abstract Property getUseAgent(); public abstract Property getUberMainClass(); + + public abstract ListProperty getPrecompiledTemplateTypes(); + + public abstract ConfigurableFileCollection getTemplateDirectories(); } diff --git a/build-logic/src/main/java/com/uwyn/rife2/gradle/Rife2Plugin.java b/build-logic/src/main/java/com/uwyn/rife2/gradle/Rife2Plugin.java index 17bfd18..985e06f 100644 --- a/build-logic/src/main/java/com/uwyn/rife2/gradle/Rife2Plugin.java +++ b/build-logic/src/main/java/com/uwyn/rife2/gradle/Rife2Plugin.java @@ -21,29 +21,35 @@ import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.ConfigurationContainer; import org.gradle.api.artifacts.dsl.DependencyHandler; import org.gradle.api.attributes.Attribute; -import org.gradle.api.attributes.AttributeContainer; import org.gradle.api.attributes.Bundling; import org.gradle.api.component.AdhocComponentWithVariants; import org.gradle.api.component.ConfigurationVariantDetails; +import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.DuplicatesStrategy; import org.gradle.api.plugins.BasePluginExtension; +import org.gradle.api.plugins.JavaApplication; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.plugins.PluginContainer; -import org.gradle.api.tasks.*; +import org.gradle.api.tasks.JavaExec; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskContainer; +import org.gradle.api.tasks.TaskProvider; import org.gradle.api.tasks.bundling.Jar; import org.gradle.api.tasks.testing.Test; import org.gradle.process.CommandLineArgumentProvider; import java.util.Collections; +import java.util.List; import java.util.Locale; +import java.util.stream.Collectors; -@SuppressWarnings({"ALL", "unused"}) public class Rife2Plugin implements Plugin { - public static final String DEFAULT_TEMPLATES_DIR = "src/main/templates"; + public static final List DEFAULT_TEMPLATES_DIRS = List.of("src/main/resources/templates"); public static final String DEFAULT_GENERATED_RIFE2_CLASSES_DIR = "generated/classes/rife2"; public static final String RIFE2_GROUP = "rife2"; public static final String WEBAPP_SRCDIR = "src/main/webapp"; + public static final String PRECOMPILE_TEMPLATES_TASK_NAME = "precompileTemplates"; @Override public void apply(Project project) { @@ -54,67 +60,88 @@ public class Rife2Plugin implements Plugin { var configurations = project.getConfigurations(); var dependencyHandler = project.getDependencies(); var tasks = project.getTasks(); + var rife2Configuration = createRife2Configuration(configurations, dependencyHandler, rife2Extension); var rife2CompilerClasspath = createRife2CompilerClasspathConfiguration(configurations, rife2Configuration); var rife2AgentClasspath = createRife2AgentConfiguration(configurations, dependencyHandler, rife2Extension); configurations.getByName(JavaPlugin.IMPLEMENTATION_CONFIGURATION_NAME).extendsFrom(rife2Configuration); - var precompileTemplates = registerPrecompileTemplateTask(project, rife2CompilerClasspath); - createRife2DevelopmentOnlyConfiguration(project, configurations, dependencyHandler, precompileTemplates); + + var precompileTemplates = registerPrecompileTemplateTask(project, rife2CompilerClasspath, rife2Extension); + createRife2DevelopmentOnlyConfiguration(project, configurations, dependencyHandler, rife2Extension.getTemplateDirectories()); exposePrecompiledTemplatesToTestTask(project, configurations, dependencyHandler, precompileTemplates); configureAgent(project, plugins, rife2Extension, rife2AgentClasspath); - registerRunTask(project, rife2Extension, rife2AgentClasspath); TaskProvider uberJarTask = registerUberJarTask(project, plugins, javaPluginExtension, rife2Extension, tasks, precompileTemplates); - bundlePrecompiledTemplatesIntoJarFile(tasks, precompileTemplates); + bundlePrecompiledTemplatesIntoJarFile(tasks, precompileTemplates, rife2Extension); + configureMavenPublishing(project, plugins, configurations, uberJarTask); } - private static void configureMavenPublishing(Project project, PluginContainer plugins, ConfigurationContainer configurations, TaskProvider uberJarTask) { + @SuppressWarnings("unchecked") + private static void configureMavenPublishing(Project project, + PluginContainer plugins, + ConfigurationContainer configurations, + TaskProvider uberJarTask) { plugins.withId("maven-publish", unused -> { - Configuration rife2UberJarElements = configurations.create("rife2UberJarElements", conf -> { + var rife2UberJarElements = configurations.create("rife2UberJarElements", conf -> { conf.setDescription("Exposes the uber jar archive of the RIFE2 web application."); conf.setCanBeResolved(false); conf.setCanBeConsumed(true); conf.getOutgoing().artifact(uberJarTask, artifact -> artifact.setClassifier("uber")); - AttributeContainer runtimeAttributes = configurations.getByName(JavaPlugin.RUNTIME_ELEMENTS_CONFIGURATION_NAME).getAttributes(); + + var runtimeAttributes = configurations.getByName(JavaPlugin.RUNTIME_ELEMENTS_CONFIGURATION_NAME).getAttributes(); conf.attributes(attrs -> { for (Attribute attribute : runtimeAttributes.keySet()) { Object value = runtimeAttributes.getAttribute(attribute); - if (value != null) { - if (Bundling.class.equals(attribute.getType())) { - attrs.attribute(Bundling.BUNDLING_ATTRIBUTE, project.getObjects().named(Bundling.class, Bundling.SHADOWED)); - } else { - //noinspection unchecked - attrs.attribute((Attribute) attribute, value); - } + //noinspection unchecked + if (Bundling.class.equals(attribute.getType())) { + attrs.attribute(Bundling.BUNDLING_ATTRIBUTE, project.getObjects().named(Bundling.class, Bundling.SHADOWED)); + } else { + attrs.attribute((Attribute) attribute, value); } } }); }); - AdhocComponentWithVariants component = (AdhocComponentWithVariants) project.getComponents().getByName("java"); + + var component = (AdhocComponentWithVariants) project.getComponents().getByName("java"); component.addVariantsFromConfiguration(rife2UberJarElements, ConfigurationVariantDetails::mapToOptional); }); } - private static void exposePrecompiledTemplatesToTestTask(Project project, ConfigurationContainer configurations, DependencyHandler dependencyHandler, TaskProvider precompileTemplates) { + private static void exposePrecompiledTemplatesToTestTask(Project project, + ConfigurationContainer configurations, + DependencyHandler dependencyHandler, + TaskProvider precompileTemplatesTask) { configurations.getByName(JavaPlugin.TEST_RUNTIME_ONLY_CONFIGURATION_NAME) - .getDependencies() - .add(dependencyHandler.create(project.files(precompileTemplates))); + .getDependencies() + .add(dependencyHandler.create(project.files(precompileTemplatesTask))); } - private static void bundlePrecompiledTemplatesIntoJarFile(TaskContainer tasks, TaskProvider precompileTemplates) { - tasks.named("jar", Jar.class, jar -> jar.from(precompileTemplates)); + private static void bundlePrecompiledTemplatesIntoJarFile(TaskContainer tasks, + TaskProvider precompileTemplatesTask, + Rife2Extension rife2Extension) { + tasks.named("jar", Jar.class, jar -> { + jar.from(precompileTemplatesTask); + // This isn't great because it needs to be hardcoded, in order to avoid the templates + // declared in `src/main/resources/templates` to be included in the jar file. + // which means that if for whatever reason the user also uses the same directory for + // something else, it will be excluded from the jar file. + rife2Extension.getPrecompiledTemplateTypes().get().forEach(templateType -> jar.exclude("/templates/**." + templateType.identifier().toLowerCase()) + ); + }); } private void createRife2DevelopmentOnlyConfiguration(Project project, - ConfigurationContainer configurations, + ConfigurationContainer configurations, DependencyHandler dependencies, - TaskProvider precompileTemplatesTask) { - Configuration rife2DevelopmentOnly = configurations.create("rife2DevelopmentOnly", conf -> { + ConfigurableFileCollection templateDirectories) { + var rife2DevelopmentOnly = configurations.create("rife2DevelopmentOnly", conf -> { conf.setDescription("Dependencies which should only be visible when running the application in development mode (and not in tests)."); conf.setCanBeConsumed(false); conf.setCanBeResolved(false); }); - rife2DevelopmentOnly.getDependencies().add(dependencies.create(project.files(precompileTemplatesTask))); + rife2DevelopmentOnly.getDependencies().addAllLater(templateDirectories.getElements().map(locations -> + locations.stream().map(fs -> dependencies.create(project.files(fs))).collect(Collectors.toList())) + ); configurations.getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME).extendsFrom(rife2DevelopmentOnly); } @@ -130,21 +157,29 @@ public class Rife2Plugin implements Plugin { var base = project.getExtensions().getByType(BasePluginExtension.class); jar.getArchiveBaseName().convention(project.provider(() -> base.getArchivesName().get() + "-uber")); jar.setDuplicatesStrategy(DuplicatesStrategy.EXCLUDE); - var runtimeClasspath = project.getConfigurations().getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); jar.from(javaPluginExtension.getSourceSets().getByName(SourceSet.MAIN_SOURCE_SET_NAME).getOutput()); jar.from(precompileTemplatesTask); jar.into("webapp", spec -> spec.from(WEBAPP_SRCDIR)); + var runtimeClasspath = project.getConfigurations().getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); jar.from(runtimeClasspath.getElements().map(e -> e.stream() - .filter(f -> f.getAsFile().getName().toLowerCase(Locale.ENGLISH).endsWith(".jar")) - .map(project::zipTree) - .toList())); + .filter(f -> f.getAsFile().getName().toLowerCase(Locale.ENGLISH).endsWith(".jar")) + .map(project::zipTree) + .toList())); + // This isn't great because it needs to be hardcoded, in order to avoid the templates + // declared in `src/main/resources/templates` to be included in the jar file. + // which means that if for whatever reason the user also uses the same directory for + // something else, it will be excluded from the jar file. + jar.exclude("templates"); plugins.withId("application", unused -> jar.manifest(manifest -> manifest.getAttributes().put("Main-Class", rife2Extension.getUberMainClass().get())) ); }); } - private static void configureAgent(Project project, PluginContainer plugins, Rife2Extension rife2Extension, Configuration rife2AgentClasspath) { + private static void configureAgent(Project project, + PluginContainer plugins, + Rife2Extension rife2Extension, + Configuration rife2AgentClasspath) { CommandLineArgumentProvider agentProvider = () -> { if (Boolean.TRUE.equals(rife2Extension.getUseAgent().get())) { return Collections.singleton("-javaagent:" + rife2AgentClasspath.getAsPath()); @@ -158,11 +193,14 @@ public class Rife2Plugin implements Plugin { private static Rife2Extension createRife2Extension(Project project) { var rife2 = project.getExtensions().create("rife2", Rife2Extension.class); rife2.getUseAgent().convention(false); - rife2.getUberMainClass().set(rife2.getMainClass() + "Uber"); + rife2.getUberMainClass().convention(project.getExtensions().getByType(JavaApplication.class).getMainClass() + .map(mainClass -> mainClass + "Uber")); + DEFAULT_TEMPLATES_DIRS.stream().forEachOrdered(dir -> rife2.getTemplateDirectories().from(project.files(dir))); return rife2; } - private static Configuration createRife2CompilerClasspathConfiguration(ConfigurationContainer configurations, Configuration rife2Configuration) { + private static Configuration createRife2CompilerClasspathConfiguration(ConfigurationContainer configurations, + Configuration rife2Configuration) { return configurations.create("rife2CompilerClasspath", conf -> { conf.setDescription("The RIFE2 compiler classpath"); conf.setCanBeConsumed(false); @@ -198,28 +236,16 @@ public class Rife2Plugin implements Plugin { } private static TaskProvider registerPrecompileTemplateTask(Project project, - Configuration rife2CompilerClasspath) { - return project.getTasks().register("precompileTemplates", PrecompileTemplates.class, task -> { + Configuration rife2CompilerClasspath, + Rife2Extension rife2Extension) { + return project.getTasks().register(PRECOMPILE_TEMPLATES_TASK_NAME, PrecompileTemplates.class, task -> { task.setGroup(RIFE2_GROUP); task.setDescription("Pre-compiles the templates."); task.getVerbose().convention(true); task.getClasspath().from(rife2CompilerClasspath); - task.getType().convention("html"); - task.getTemplatesDirectory().set(project.getLayout().getProjectDirectory().dir(DEFAULT_TEMPLATES_DIR)); + task.getTypes().convention(rife2Extension.getPrecompiledTemplateTypes()); + task.getTemplatesDirectories().from(rife2Extension.getTemplateDirectories()); task.getOutputDirectory().set(project.getLayout().getBuildDirectory().dir(DEFAULT_GENERATED_RIFE2_CLASSES_DIR)); }); } - - private static void registerRunTask(Project project, Rife2Extension rife2Extension, - Configuration rife2CompilerClasspath) { - project.getTasks().register("run", RunTask.class, task -> { - task.setGroup(RIFE2_GROUP); - task.setDescription("Runs this project as a web application."); - task.getAgentClassPath().set(rife2CompilerClasspath.getAsPath()); - task.getClasspath().from(project.getExtensions().getByType(SourceSetContainer.class) - .getByName(SourceSet.MAIN_SOURCE_SET_NAME).getRuntimeClasspath()); - task.getMainClass().set(rife2Extension.getMainClass()); - task.getTemplatesDirectory().set(project.getLayout().getProjectDirectory().dir(DEFAULT_TEMPLATES_DIR)); - }); - } } diff --git a/build-logic/src/main/java/com/uwyn/rife2/gradle/RunTask.java b/build-logic/src/main/java/com/uwyn/rife2/gradle/RunTask.java deleted file mode 100644 index c107544..0000000 --- a/build-logic/src/main/java/com/uwyn/rife2/gradle/RunTask.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2003-2021 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 - * - * https://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.uwyn.rife2.gradle; - -import org.gradle.api.DefaultTask; -import org.gradle.api.file.ConfigurableFileCollection; -import org.gradle.api.file.DirectoryProperty; -import org.gradle.api.provider.Property; -import org.gradle.api.tasks.*; -import org.gradle.process.ExecOperations; - -import javax.inject.Inject; -import java.util.List; - -@CacheableTask -public abstract class RunTask extends DefaultTask { - @Input - public abstract Property getAgentClassPath(); - - @Classpath - public abstract ConfigurableFileCollection getClasspath(); - - @Inject - protected abstract ExecOperations getExecOperations(); - - @Input - public abstract Property getMainClass(); - - @InputDirectory - @PathSensitive(PathSensitivity.RELATIVE) - public abstract DirectoryProperty getTemplatesDirectory(); - - @TaskAction - public void run() { - getExecOperations().javaexec(run -> { - run.setClasspath(getProject().getObjects().fileCollection().from(getTemplatesDirectory()).plus(getClasspath())); - run.getMainClass().set(getMainClass()); - run.args(List.of("-javaagent:" + getAgentClassPath().get())); - }); - } -} \ No newline at end of file diff --git a/build-logic/src/main/java/com/uwyn/rife2/gradle/TemplateType.java b/build-logic/src/main/java/com/uwyn/rife2/gradle/TemplateType.java new file mode 100644 index 0000000..6fff9a7 --- /dev/null +++ b/build-logic/src/main/java/com/uwyn/rife2/gradle/TemplateType.java @@ -0,0 +1,25 @@ +package com.uwyn.rife2.gradle; + +import java.io.Serial; +import java.io.Serializable; + +public class TemplateType implements Serializable { + @Serial private static final long serialVersionUID = -2736320275307140837L; + + public static TemplateType HTML = new TemplateType("html"); + public static TemplateType JSON = new TemplateType("json"); + public static TemplateType SVG = new TemplateType("svg"); + public static TemplateType XML = new TemplateType("xml"); + public static TemplateType TXT = new TemplateType("txt"); + public static TemplateType SQL = new TemplateType("sql"); + + private final String identifier_; + + public TemplateType(String identifier) { + identifier_ = identifier; + } + + public String identifier() { + return identifier_; + } +} \ No newline at end of file diff --git a/build-logic/src/test-projects/minimal/build.gradle b/build-logic/src/test-projects/minimal/build.gradle new file mode 100644 index 0000000..2212c0f --- /dev/null +++ b/build-logic/src/test-projects/minimal/build.gradle @@ -0,0 +1,42 @@ +import com.uwyn.rife2.gradle.TemplateType + +plugins { + id("application") + id("com.uwyn.rife2") +} + +base { + archivesName = "hello" + version = 1.0 + group = "com.example" +} + +application { + mainClass = "hello.App" +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +repositories { + mavenCentral() +} + +rife2 { + version = "1.4.0" + useAgent = true + precompiledTemplateTypes.add(TemplateType.HTML) + templateDirectories.from(file("src/main/templates")) +} + +dependencies { + runtimeOnly("org.eclipse.jetty:jetty-server:11.0.13") + runtimeOnly("org.eclipse.jetty:jetty-servlet:11.0.13") + runtimeOnly("org.slf4j:slf4j-simple:2.0.5") + + testImplementation("org.jsoup:jsoup:1.15.3") + testImplementation("org.junit.jupiter:junit-jupiter:5.9.1") +} diff --git a/build-logic/src/test-projects/minimal/settings.gradle b/build-logic/src/test-projects/minimal/settings.gradle new file mode 100644 index 0000000..e87ab3c --- /dev/null +++ b/build-logic/src/test-projects/minimal/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'minimal' diff --git a/build-logic/src/test-projects/minimal/src/main/java/hello/App.java b/build-logic/src/test-projects/minimal/src/main/java/hello/App.java new file mode 100644 index 0000000..43a1870 --- /dev/null +++ b/build-logic/src/test-projects/minimal/src/main/java/hello/App.java @@ -0,0 +1,16 @@ +package hello; + +import rife.engine.*; + +public class App extends Site { + public void setup() { + var hello = get("/hello", c -> c.print(c.template("hello"))); + get("/", c -> c.redirect(hello)); + } + + public static void main(String[] args) { + new Server() + .staticResourceBase("src/main/webapp") + .start(new App()); + } +} diff --git a/build-logic/src/test-projects/minimal/src/main/resources/templates/world.html b/build-logic/src/test-projects/minimal/src/main/resources/templates/world.html new file mode 100644 index 0000000..59ff81b --- /dev/null +++ b/build-logic/src/test-projects/minimal/src/main/resources/templates/world.html @@ -0,0 +1,11 @@ + + + + + <!--v title-->Hello<!--/v--> + + + +

Hello World

+ + diff --git a/build-logic/src/test-projects/minimal/src/main/templates/hello.html b/build-logic/src/test-projects/minimal/src/main/templates/hello.html new file mode 100644 index 0000000..59ff81b --- /dev/null +++ b/build-logic/src/test-projects/minimal/src/main/templates/hello.html @@ -0,0 +1,11 @@ + + + + + <!--v title-->Hello<!--/v--> + + + +

Hello World

+ + diff --git a/build-logic/src/test-projects/minimal/src/main/webapp/css/style.css b/build-logic/src/test-projects/minimal/src/main/webapp/css/style.css new file mode 100644 index 0000000..52bf6c7 --- /dev/null +++ b/build-logic/src/test-projects/minimal/src/main/webapp/css/style.css @@ -0,0 +1,21 @@ +:root { + /* fonts */ + --main-font: sans-serif; + + /* font sizes */ + --main-font-size: 18px; + + /* colors */ + --main-background-color: #0d0d0d; + --main-text-color: #d0d0d0; + + /* margins and padding */ + --content-padding: 2em; +} +body { + background: var(--main-background-color); + font-family: var(--main-font); + font-style: var(--main-font-size); + color: var(--main-text-color); + padding: var(--content-padding); +} \ No newline at end of file diff --git a/build-logic/src/test-projects/minimal/src/test/java/hello/AppTest.java b/build-logic/src/test-projects/minimal/src/test/java/hello/AppTest.java new file mode 100644 index 0000000..45a0763 --- /dev/null +++ b/build-logic/src/test-projects/minimal/src/test/java/hello/AppTest.java @@ -0,0 +1,24 @@ +/* + * This Java source file was generated by the Gradle 'init' task. + */ +package hello; + +import org.junit.jupiter.api.Test; +import rife.test.MockConversation; + +import static org.junit.jupiter.api.Assertions.*; + +public class AppTest { + @Test + void verifyRoot() { + var m = new MockConversation(new App()); + assertEquals(m.doRequest("/").getStatus(), 302); + } + + @Test + void verifyHello() { + var m = new MockConversation(new App()); + assertEquals("Hello", m.doRequest("/hello") + .getTemplate().getValue("title")); + } +} diff --git a/build-logic/src/test/groovy/com/uwyn/rife2/gradle/AbstractFunctionalTest.groovy b/build-logic/src/test/groovy/com/uwyn/rife2/gradle/AbstractFunctionalTest.groovy new file mode 100644 index 0000000..7629d3f --- /dev/null +++ b/build-logic/src/test/groovy/com/uwyn/rife2/gradle/AbstractFunctionalTest.groovy @@ -0,0 +1,184 @@ +package com.uwyn.rife2.gradle + +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome +import org.gradle.util.GFileUtils +import org.gradle.util.GradleVersion +import spock.lang.Specification +import spock.lang.TempDir + +import java.nio.file.Path + +abstract class AbstractFunctionalTest extends Specification { + + private final String gradleVersion = System.getProperty("gradleVersion", GradleVersion.current().version) + + @TempDir + Path testDirectory + + boolean debug + + private StringWriter outputWriter + private StringWriter errorOutputWriter + private String output + private String errorOutput + + BuildResult result + + Path path(String... pathElements) { + Path cur = testDirectory + pathElements.each { + cur = cur.resolve(it) + } + cur + } + + File file(String... pathElements) { + path(pathElements).toFile() + } + + File getGroovyBuildFile() { + file("build.gradle") + } + + File getBuildFile() { + groovyBuildFile + } + + File getKotlinBuildFile() { + file("build.gradle.kts") + } + + File getGroovySettingsFile() { + file("settings.gradle") + } + + File getKotlinSettingsFile() { + file("settings.gradle.kts") + } + + File getSettingsFile() { + groovySettingsFile + } + + void run(String... args) { + try { + result = newRunner(args) + .build() + } finally { + recordOutputs() + } + } + + void outputContains(String text) { + assert output.normalize().contains(text.normalize()) + } + + void outputDoesNotContain(String text) { + assert !output.normalize().contains(text.normalize()) + } + + void errorOutputContains(String text) { + assert errorOutput.normalize().contains(text.normalize()) + } + + void tasks(@DelegatesTo(value = TaskExecutionGraph, strategy = Closure.DELEGATE_FIRST) Closure spec) { + def graph = new TaskExecutionGraph() + spec.delegate = graph + spec.resolveStrategy = Closure.DELEGATE_FIRST + spec() + } + + private void recordOutputs() { + output = outputWriter.toString() + errorOutput = errorOutputWriter.toString() + } + + private GradleRunner newRunner(String... args) { + outputWriter = new StringWriter() + errorOutputWriter = new StringWriter() + ArrayList autoArgs = computeAutoArgs() + def runner = GradleRunner.create() + .forwardStdOutput(tee(new OutputStreamWriter(System.out), outputWriter)) + .forwardStdError(tee(new OutputStreamWriter(System.err), errorOutputWriter)) + .withPluginClasspath() + .withProjectDir(testDirectory.toFile()) + .withArguments([*autoArgs, *args]) + if (gradleVersion) { + runner.withGradleVersion(gradleVersion) + } + if (debug) { + runner.withDebug(true) + } + runner + } + + private ArrayList computeAutoArgs() { + List autoArgs = [ + "-s", + "--console=verbose" + ] + if (Boolean.getBoolean("config.cache")) { + autoArgs << '--configuration-cache' + } + autoArgs + } + + private static Writer tee(Writer one, Writer two) { + return TeeWriter.of(one, two) + } + + void fails(String... args) { + try { + result = newRunner(args) + .buildAndFail() + } finally { + recordOutputs() + } + } + + private class TaskExecutionGraph { + void succeeded(String... tasks) { + tasks.each { task -> + contains(task) + assert result.task(task).outcome == TaskOutcome.SUCCESS + } + } + + void failed(String... tasks) { + tasks.each { task -> + contains(task) + assert result.task(task).outcome == TaskOutcome.FAILED + } + } + + void skipped(String... tasks) { + tasks.each { task -> + contains(task) + assert result.task(task).outcome == TaskOutcome.SKIPPED + } + } + + void contains(String... tasks) { + tasks.each { task -> + assert result.task(task) != null: "Expected to find task $task in the graph but it was missing" + } + } + + void doesNotContain(String... tasks) { + tasks.each { task -> + assert result.task(task) == null: "Task $task should be missing from the task graph but it was found with an outcome of ${result.task(task).outcome}" + } + } + } + + void usesProject(String name) { + File sampleDir = new File("src/test-projects/$name") + GFileUtils.copyDirectory(sampleDir, testDirectory.toFile()) + } + + File file(String path) { + new File(testDirectory.toFile(), path) + } +} diff --git a/build-logic/src/test/groovy/com/uwyn/rife2/gradle/PackagingTest.groovy b/build-logic/src/test/groovy/com/uwyn/rife2/gradle/PackagingTest.groovy new file mode 100644 index 0000000..e3aeb36 --- /dev/null +++ b/build-logic/src/test/groovy/com/uwyn/rife2/gradle/PackagingTest.groovy @@ -0,0 +1,38 @@ +package com.uwyn.rife2.gradle + +import java.nio.file.FileSystems +import java.nio.file.Files + +class PackagingTest extends AbstractFunctionalTest { + def setup() { + usesProject("minimal") + } + + def "#archive contains compiled resources"() { + def jarFile = file(archive).toPath() + when: + run task + + then: "compiles templates are found in the archive" + tasks { + succeeded ":${Rife2Plugin.PRECOMPILE_TEMPLATES_TASK_NAME}" + } + Files.exists(jarFile) + try (def fs = FileSystems.newFileSystem(jarFile, [:])) { + fs.getRootDirectories().each { + Files.walk(it).forEach { path -> + println path + } + } + assert Files.exists(fs.getPath("/rife/template/html/hello.class")) + assert Files.exists(fs.getPath("/rife/template/html/world.class")) + assert !Files.exists(fs.getPath("/templates/hello.html")) + assert !Files.exists(fs.getPath("/templates/world.html")) + } + + where: + task | archive + 'jar' | 'build/libs/hello-1.0.jar' + 'uberJar' | 'build/libs/hello-uber-1.0.jar' + } +} diff --git a/build-logic/src/test/groovy/com/uwyn/rife2/gradle/TeeWriter.groovy b/build-logic/src/test/groovy/com/uwyn/rife2/gradle/TeeWriter.groovy new file mode 100644 index 0000000..89f60c1 --- /dev/null +++ b/build-logic/src/test/groovy/com/uwyn/rife2/gradle/TeeWriter.groovy @@ -0,0 +1,81 @@ +package com.uwyn.rife2.gradle + +import groovy.transform.CompileStatic + +@CompileStatic +class TeeWriter extends Writer { + private final Writer one + private final Writer two + + static TeeWriter of(Writer one, Writer two) { + new TeeWriter(one, two) + } + + private TeeWriter(Writer one, Writer two) { + this.one = one + this.two = two + } + + @Override + void write(int c) throws IOException { + try { + one.write(c) + } finally { + two.write(c) + } + } + + @Override + void write(char[] cbuf) throws IOException { + try { + one.write(cbuf) + } finally { + two.write(cbuf) + } + } + + @Override + void write(char[] cbuf, int off, int len) throws IOException { + try { + one.write(cbuf, off, len) + } finally { + two.write(cbuf, off, len) + } + } + + @Override + void write(String str) throws IOException { + try { + one.write(str) + } finally { + two.write(str) + } + } + + @Override + void write(String str, int off, int len) throws IOException { + try { + one.write(str, off, len) + } finally { + two.write(str, off, len) + } + } + + @Override + void flush() throws IOException { + try { + one.flush() + } finally { + two.flush() + } + } + + @Override + void close() throws IOException { + try { + one.close() + } finally { + two.close() + } + } +} diff --git a/build-logic/src/test/groovy/com/uwyn/rife2/gradle/TemplateCompilationTest.groovy b/build-logic/src/test/groovy/com/uwyn/rife2/gradle/TemplateCompilationTest.groovy new file mode 100644 index 0000000..b935f04 --- /dev/null +++ b/build-logic/src/test/groovy/com/uwyn/rife2/gradle/TemplateCompilationTest.groovy @@ -0,0 +1,60 @@ +package com.uwyn.rife2.gradle + +class TemplateCompilationTest extends AbstractFunctionalTest { + def setup() { + usesProject("minimal") + } + + def "doesn't precompile templates when calling `run`"() { + given: + buildFile << """ + tasks.named("run") { + doFirst { + throw new RuntimeException("force stop") + } + } + """ + when: + fails 'run' + + then: "precompile templates task must not be present in task graph" + errorOutputContains("force stop") + tasks { + doesNotContain ":${Rife2Plugin.PRECOMPILE_TEMPLATES_TASK_NAME}" + } + } + + def "`run` task classpath includes template sources"() { + given: + buildFile << """ + tasks.register("dumpRunClasspath") { + doLast { + def rootPath = rootProject.projectDir.toPath() + tasks.named("run").get().classpath.files.each { + println "Classpath entry: \${rootPath.relativize(it.toPath())}" + } + } + } + """ + + when: + run("dumpRunClasspath") + + then: "template sources must be present in the classpath" + outputContains("Classpath entry: src/main/templates") + outputContains("Classpath entry: src/main/resources/templates") + } + + def "compiles templates when running #task"() { + when: + run task + + then: "precompile templates task must be present in task graph" + tasks { + succeeded ":${Rife2Plugin.PRECOMPILE_TEMPLATES_TASK_NAME}" + } + + where: + task << ['jar', 'test', 'uberJar'] + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..3a8c200 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,17 @@ +[versions] +jetty = "11.0.13" +jsoup = "1.15.3" +junit-jupiter = "5.9.1" +slf4j = "2.0.5" +spock = "2.3-groovy-3.0" + +[libraries] +jetty-server = { module = "org.eclipse.jetty:jetty-server", version.ref = "jetty" } +jetty-servlet = { module = "org.eclipse.jetty:jetty-servlet", version.ref = "jetty" } +jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } +junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" } +slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" } +spock-core = { module = "org.spockframework:spock-core", version.ref = "spock" } + +[bundles] +jetty = [ "jetty-server", "jetty-servlet" ] diff --git a/settings.gradle.kts b/settings.gradle.kts index f3be8d5..b4af770 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,8 @@ pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + } includeBuild("build-logic") } diff --git a/war/build.gradle.kts b/war/build.gradle.kts index 477784d..206f003 100644 --- a/war/build.gradle.kts +++ b/war/build.gradle.kts @@ -2,10 +2,9 @@ plugins { war } -version = 1.0 - base { archivesName.set("hello") + version = 1.0 } repositories { @@ -21,4 +20,4 @@ tasks.war { webAppDirectory.set(file("../app/src/main/webapp")) webXml = file("src/web.xml") rootSpec.exclude("**/jetty*.jar", "**/slf4j*.jar", "**/rife2*-agent.jar") -} +} \ No newline at end of file