diff --git a/unified-prototype/gradle/wrapper/gradle-wrapper.jar b/unified-prototype/gradle/wrapper/gradle-wrapper.jar index e6441136..a4b76b95 100644 Binary files a/unified-prototype/gradle/wrapper/gradle-wrapper.jar and b/unified-prototype/gradle/wrapper/gradle-wrapper.jar differ diff --git a/unified-prototype/gradle/wrapper/gradle-wrapper.properties b/unified-prototype/gradle/wrapper/gradle-wrapper.properties index 6a92d516..e63530c2 100644 --- a/unified-prototype/gradle/wrapper/gradle-wrapper.properties +++ b/unified-prototype/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions-snapshots/gradle-8.10-20240719001820+0000-bin.zip +distributionUrl=https\://services.gradle.org/distributions-snapshots/gradle-8.11-20240812191308+0000-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/unified-prototype/unified-plugin/plugin-common/build.gradle.kts b/unified-prototype/unified-plugin/plugin-common/build.gradle.kts index b9fe61aa..321f2528 100644 --- a/unified-prototype/unified-plugin/plugin-common/build.gradle.kts +++ b/unified-prototype/unified-plugin/plugin-common/build.gradle.kts @@ -1,6 +1,7 @@ plugins { `kotlin-dsl` id("build-logic.publishing") + groovy // For spock testing } description = "Common APIs and implementation classes shared by the ecosystem specific declarative prototypes" @@ -8,9 +9,36 @@ description = "Common APIs and implementation classes shared by the ecosystem sp dependencies { implementation(libs.android.agp.application) + implementation("commons-io:commons-io:2.8.0") implementation(gradleApi()) } +testing { + suites { + @Suppress("UnstableApiUsage") + val test by getting(JvmTestSuite::class) { + useSpock("2.2-groovy-3.0") + + dependencies { + implementation("commons-io:commons-io:2.8.0") + } + } + + @Suppress("UnstableApiUsage") + val integTest by registering(JvmTestSuite::class) { + useSpock("2.2-groovy-3.0") + + dependencies { + implementation("commons-io:commons-io:2.8.0") + implementation(project(":plugin-jvm")) + implementation(project()) + } + } + + tasks.getByPath("check").dependsOn(integTest) + } +} + gradlePlugin { plugins { create("common") { diff --git a/unified-prototype/unified-plugin/plugin-common/output/build.gradle.dcl b/unified-prototype/unified-plugin/plugin-common/output/build.gradle.dcl new file mode 100644 index 00000000..1eecd518 --- /dev/null +++ b/unified-prototype/unified-plugin/plugin-common/output/build.gradle.dcl @@ -0,0 +1,8 @@ +javaLibrary { + javaVersion = 21 + + dependencies { + implementation(project(":java-util")) + implementation("com.google.guava:guava:32.1.3-jre") + } +} diff --git a/unified-prototype/unified-plugin/plugin-common/output/src/main/java/com/example/lib/Library.java b/unified-prototype/unified-plugin/plugin-common/output/src/main/java/com/example/lib/Library.java new file mode 100644 index 00000000..b6d22b4f --- /dev/null +++ b/unified-prototype/unified-plugin/plugin-common/output/src/main/java/com/example/lib/Library.java @@ -0,0 +1,13 @@ +package com.example.lib; + +import com.google.common.collect.ImmutableList; + +public class Library { + public Iterable getMessages() { + // Verify that Guava is available + ImmutableList.Builder builder = ImmutableList.builder(); + builder.add("Hello from Java " + System.getProperty("java.version")); + + return builder.build(); + } +} diff --git a/unified-prototype/unified-plugin/plugin-common/src/integTest/groovy/org/gradle/util/ResourceLoaderIntegrationTest.groovy b/unified-prototype/unified-plugin/plugin-common/src/integTest/groovy/org/gradle/util/ResourceLoaderIntegrationTest.groovy new file mode 100644 index 00000000..b73aae1e --- /dev/null +++ b/unified-prototype/unified-plugin/plugin-common/src/integTest/groovy/org/gradle/util/ResourceLoaderIntegrationTest.groovy @@ -0,0 +1,29 @@ +package org.gradle.util + +import org.apache.commons.io.FileUtils +import spock.lang.Specification + +class ResourceLoaderIntegrationTest extends Specification { + File outputDir + + def setup() { + outputDir = new File("build/tmp/integTest/output").tap { deleteDir() } + } + + def "can load resource from jar file"() { + given: + ResourceLoader resourceLoader = new ResourceLoader() + + when: + resourceLoader.extractResourcesFromJar("templates/java-library", outputDir) + + then: + assertOutputIs(['build.gradle.dcl', 'src/main/java/com/example/lib/Library.java']) + } + + private void assertOutputIs(List expectedRelativePaths) { + def actualPaths = FileUtils.listFiles(outputDir, null, true)*.path + def expectedPaths = expectedRelativePaths.collect { "${outputDir.toPath()}/$it".toString() } + assert actualPaths == expectedPaths + } +} diff --git a/unified-prototype/unified-plugin/plugin-common/src/main/java/org/gradle/api/experimental/buildinit/StaticProjectGenerator.java b/unified-prototype/unified-plugin/plugin-common/src/main/java/org/gradle/api/experimental/buildinit/StaticProjectGenerator.java new file mode 100644 index 00000000..6efa8bc1 --- /dev/null +++ b/unified-prototype/unified-plugin/plugin-common/src/main/java/org/gradle/api/experimental/buildinit/StaticProjectGenerator.java @@ -0,0 +1,31 @@ +package org.gradle.api.experimental.buildinit; + +import org.gradle.api.file.Directory; +import org.gradle.buildinit.projectspecs.InitProjectConfig; +import org.gradle.buildinit.projectspecs.InitProjectGenerator; +import org.gradle.util.ResourceLoader; + +/** + * An {@link InitProjectGenerator} that generates a project from a static template packaged + * as resources files in the {@link #TEMPLATES_ROOT} directory. + */ +@SuppressWarnings("UnstableApiUsage") +public final class StaticProjectGenerator implements InitProjectGenerator { + private static final String TEMPLATES_ROOT = "templates"; + + @Override + public void generate(InitProjectConfig config, Directory projectDir) { + if (!(config.getProjectSpec() instanceof StaticProjectSpec projectSpec)) { + throw new IllegalArgumentException("Unknown project type: " + config.getProjectSpec().getDisplayName() + " (" + config.getProjectSpec().getClass().getName() + ")"); + } + + String templatePath = TEMPLATES_ROOT + "/" + projectSpec.getTemplatePath(); + ResourceLoader resourceLoader = new ResourceLoader(); + + try { + resourceLoader.extractResourcesFromJar(templatePath, projectDir.getAsFile()); + } catch (Exception e) { + throw new RuntimeException("Error extracting resources for: '" + projectSpec.getDisplayName() + "' from: '" + templatePath + "'!", e); + } + } +} diff --git a/unified-prototype/unified-plugin/plugin-common/src/main/java/org/gradle/api/experimental/buildinit/StaticProjectSpec.java b/unified-prototype/unified-plugin/plugin-common/src/main/java/org/gradle/api/experimental/buildinit/StaticProjectSpec.java new file mode 100644 index 00000000..135e5a9b --- /dev/null +++ b/unified-prototype/unified-plugin/plugin-common/src/main/java/org/gradle/api/experimental/buildinit/StaticProjectSpec.java @@ -0,0 +1,36 @@ +package org.gradle.api.experimental.buildinit; + +import org.gradle.buildinit.projectspecs.InitProjectParameter; +import org.gradle.buildinit.projectspecs.InitProjectSpec; + +import java.util.Collections; +import java.util.List; + +/** + * An {@link InitProjectSpec} that represents a project that can be generated from a static template + * using the {@link StaticProjectGenerator} + */ +@SuppressWarnings("UnstableApiUsage") +public final class StaticProjectSpec implements InitProjectSpec { + private final String templatePath; + private final String displayName; + + public StaticProjectSpec(String templatePath, String displayName) { + this.templatePath = templatePath; + this.displayName = displayName; + } + + @Override + public String getDisplayName() { + return displayName; + } + + @Override + public List> getParameters() { + return Collections.emptyList(); + } + + public String getTemplatePath() { + return templatePath; + } +} diff --git a/unified-prototype/unified-plugin/plugin-common/src/main/java/org/gradle/util/ResourceLoader.java b/unified-prototype/unified-plugin/plugin-common/src/main/java/org/gradle/util/ResourceLoader.java new file mode 100644 index 00000000..a38e416a --- /dev/null +++ b/unified-prototype/unified-plugin/plugin-common/src/main/java/org/gradle/util/ResourceLoader.java @@ -0,0 +1,61 @@ +package org.gradle.util; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.JarURLConnection; +import java.net.URL; +import java.util.Iterator; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * Static util class containing methods for loading resources from the classpath. + */ +public final class ResourceLoader { + /** + * Recursively extracts the contents of a directory in a jar file on the classpath to a specified directory. + * + * @param relativePath path to the source directory within the jar file + * @param destDir target directory to extract the contents to + * @throws IOException if an I/O error occurs + */ + public void extractResourcesFromJar(String relativePath, File destDir) throws IOException { + URL jarDirURL = ResourceLoader.class.getClassLoader().getResource(relativePath); + if (jarDirURL == null) { + throw new IllegalArgumentException("Directory: '" + relativePath + "' not found on classpath."); + } + JarFile jarFile = ((JarURLConnection) jarDirURL.openConnection()).getJarFile(); + System.out.println("Using URL: " + jarDirURL); + + Iterator iterator = jarFile.entries().asIterator(); + while (iterator.hasNext()) { + JarEntry entry = iterator.next(); + String entryName = entry.getName(); + + if (entryName.startsWith(relativePath)) { + String entrySuffix = entryName.substring(relativePath.length()); + System.out.println("Entry: " + entry + " name: " + entryName + " suffix: " + entrySuffix); + File destFile = new File(destDir, entrySuffix); + System.out.println("Dest file: " + destFile); + + if (entry.isDirectory()) { + System.out.println("Is directory"); + FileUtils.forceMkdir(destFile); + System.out.println("Created directory"); + } else { + System.out.println("Is file"); + try (InputStream is = jarFile.getInputStream(entry); + FileOutputStream fos = new FileOutputStream(destFile)) { + IOUtils.copy(is, fos); + } + System.out.println("Copied file"); + } + } + } + } +} diff --git a/unified-prototype/unified-plugin/plugin-common/src/test/groovy/org/gradle/util/ResourceLoaderTest.groovy b/unified-prototype/unified-plugin/plugin-common/src/test/groovy/org/gradle/util/ResourceLoaderTest.groovy new file mode 100644 index 00000000..482f957a --- /dev/null +++ b/unified-prototype/unified-plugin/plugin-common/src/test/groovy/org/gradle/util/ResourceLoaderTest.groovy @@ -0,0 +1,20 @@ +package org.gradle.util + +import org.apache.commons.io.FileUtils + +import spock.lang.Specification + +class ResourceLoaderTest extends Specification { + def "can load resource from jar file"() { + given: + File output = new File("output").tap { mkdirs() } + ResourceLoader resourceLoader = new ResourceLoader(this.getClass().getClassLoader()) + + when: + File templatesDir = resourceLoader.getResource("templates/java-library") + FileUtils.copyDirectory(templatesDir, output) + + then: + FileUtils.listFiles(output, null, true)*.path == ['output/build.gradle.dcl', 'output/src/main/java/com/example/lib/Library.java'] + } +} diff --git a/unified-prototype/unified-plugin/plugin-common/src/test/resources/templates/java-library/build.gradle.dcl b/unified-prototype/unified-plugin/plugin-common/src/test/resources/templates/java-library/build.gradle.dcl new file mode 100644 index 00000000..1eecd518 --- /dev/null +++ b/unified-prototype/unified-plugin/plugin-common/src/test/resources/templates/java-library/build.gradle.dcl @@ -0,0 +1,8 @@ +javaLibrary { + javaVersion = 21 + + dependencies { + implementation(project(":java-util")) + implementation("com.google.guava:guava:32.1.3-jre") + } +} diff --git a/unified-prototype/unified-plugin/plugin-common/src/test/resources/templates/java-library/src/main/java/com/example/lib/Library.java b/unified-prototype/unified-plugin/plugin-common/src/test/resources/templates/java-library/src/main/java/com/example/lib/Library.java new file mode 100644 index 00000000..b6d22b4f --- /dev/null +++ b/unified-prototype/unified-plugin/plugin-common/src/test/resources/templates/java-library/src/main/java/com/example/lib/Library.java @@ -0,0 +1,13 @@ +package com.example.lib; + +import com.google.common.collect.ImmutableList; + +public class Library { + public Iterable getMessages() { + // Verify that Guava is available + ImmutableList.Builder builder = ImmutableList.builder(); + builder.add("Hello from Java " + System.getProperty("java.version")); + + return builder.build(); + } +} diff --git a/unified-prototype/unified-plugin/plugin-jvm/build.gradle.kts b/unified-prototype/unified-plugin/plugin-jvm/build.gradle.kts index 47192370..40506b4c 100644 --- a/unified-prototype/unified-plugin/plugin-jvm/build.gradle.kts +++ b/unified-prototype/unified-plugin/plugin-jvm/build.gradle.kts @@ -7,7 +7,9 @@ description = "Implements the declarative JVM DSL prototype" dependencies { implementation(project(":plugin-common")) + implementation("commons-io:commons-io:2.8.0") implementation("org.gradle.toolchains:foojay-resolver:0.8.0") + implementation(gradleApi()) } gradlePlugin { diff --git a/unified-prototype/unified-plugin/plugin-jvm/src/main/java/org/gradle/api/experimental/buildinit/JVMProjectSource.java b/unified-prototype/unified-plugin/plugin-jvm/src/main/java/org/gradle/api/experimental/buildinit/JVMProjectSource.java new file mode 100644 index 00000000..64d86d1b --- /dev/null +++ b/unified-prototype/unified-plugin/plugin-jvm/src/main/java/org/gradle/api/experimental/buildinit/JVMProjectSource.java @@ -0,0 +1,27 @@ +package org.gradle.api.experimental.buildinit; + +import java.util.Arrays; +import java.util.List; + +import org.gradle.buildinit.projectspecs.InitProjectGenerator; +import org.gradle.buildinit.projectspecs.InitProjectSpec; +import org.gradle.buildinit.projectspecs.InitProjectSource; + +/** + * A {@link InitProjectSource} of project specifications for JVM projects. + */ +@SuppressWarnings("UnstableApiUsage") +public final class JVMProjectSource implements InitProjectSource { + @Override + public List getProjectSpecs() { + return Arrays.asList( + new StaticProjectSpec("java-library", "Declarative Java Library Project"), + new StaticProjectSpec("java-application", "Declarative Java Application Project") + ); + } + + @Override + public InitProjectGenerator getProjectGenerator() { + return new StaticProjectGenerator(); + } +} diff --git a/unified-prototype/unified-plugin/plugin-jvm/src/main/java/org/gradle/api/experimental/buildinit/package-info.java b/unified-prototype/unified-plugin/plugin-jvm/src/main/java/org/gradle/api/experimental/buildinit/package-info.java new file mode 100644 index 00000000..01fe064b --- /dev/null +++ b/unified-prototype/unified-plugin/plugin-jvm/src/main/java/org/gradle/api/experimental/buildinit/package-info.java @@ -0,0 +1,2 @@ +@org.gradle.api.NonNullApi +package org.gradle.api.experimental.buildinit; diff --git a/unified-prototype/unified-plugin/plugin-jvm/src/main/resources/META-INF/services/org.gradle.buildinit.projectspecs.InitProjectSource b/unified-prototype/unified-plugin/plugin-jvm/src/main/resources/META-INF/services/org.gradle.buildinit.projectspecs.InitProjectSource new file mode 100644 index 00000000..a207e4bc --- /dev/null +++ b/unified-prototype/unified-plugin/plugin-jvm/src/main/resources/META-INF/services/org.gradle.buildinit.projectspecs.InitProjectSource @@ -0,0 +1 @@ +org.gradle.api.experimental.buildinit.JVMProjectSource diff --git a/unified-prototype/unified-plugin/plugin-jvm/src/main/resources/templates/java-application/build.gradle.dcl b/unified-prototype/unified-plugin/plugin-jvm/src/main/resources/templates/java-application/build.gradle.dcl new file mode 100644 index 00000000..866fbc68 --- /dev/null +++ b/unified-prototype/unified-plugin/plugin-jvm/src/main/resources/templates/java-application/build.gradle.dcl @@ -0,0 +1,21 @@ +package templates.`java-application` + +javaApplication { + // compile for 17 + javaVersion = 17 + mainClass = "com.example.App" + + dependencies { + implementation(project(":java-util")) + implementation("com.google.guava:guava:32.1.3-jre") + } + + testing { + // test on 21 + testJavaVersion = 21 + + dependencies { + implementation("org.junit.jupiter:junit-jupiter:5.10.2") + } + } +} diff --git a/unified-prototype/unified-plugin/plugin-jvm/src/main/resources/templates/java-application/src/main/java/com/example/App.java b/unified-prototype/unified-plugin/plugin-jvm/src/main/resources/templates/java-application/src/main/java/com/example/App.java new file mode 100644 index 00000000..4df68009 --- /dev/null +++ b/unified-prototype/unified-plugin/plugin-jvm/src/main/resources/templates/java-application/src/main/java/com/example/App.java @@ -0,0 +1,19 @@ +package com.example; + +import com.example.utils.Utils; +import com.google.common.collect.ImmutableList; + +public class App { + public static void main(String[] args) { + // Verify that Guava is available + ImmutableList.Builder builder = ImmutableList.builder(); + builder.add("Hello from Java " + System.getProperty("java.version")); + + // Verify that the Java library is available + Utils utils = new Utils(); + builder.add(utils.getWelcome()); + + ImmutableList messages = builder.build(); + messages.forEach(System.out::println); + } +} \ No newline at end of file diff --git a/unified-prototype/unified-plugin/plugin-jvm/src/main/resources/templates/java-application/src/test/java/com/example/AppTest.java b/unified-prototype/unified-plugin/plugin-jvm/src/main/resources/templates/java-application/src/test/java/com/example/AppTest.java new file mode 100644 index 00000000..5541eac6 --- /dev/null +++ b/unified-prototype/unified-plugin/plugin-jvm/src/main/resources/templates/java-application/src/test/java/com/example/AppTest.java @@ -0,0 +1,10 @@ +package com.example; + +import org.junit.jupiter.api.Test; + +public class AppTest { + @Test + void appHasATest() { + assert true; + } +} diff --git a/unified-prototype/unified-plugin/plugin-jvm/src/main/resources/templates/java-library/build.gradle.dcl b/unified-prototype/unified-plugin/plugin-jvm/src/main/resources/templates/java-library/build.gradle.dcl new file mode 100644 index 00000000..1eecd518 --- /dev/null +++ b/unified-prototype/unified-plugin/plugin-jvm/src/main/resources/templates/java-library/build.gradle.dcl @@ -0,0 +1,8 @@ +javaLibrary { + javaVersion = 21 + + dependencies { + implementation(project(":java-util")) + implementation("com.google.guava:guava:32.1.3-jre") + } +} diff --git a/unified-prototype/unified-plugin/plugin-jvm/src/main/resources/templates/java-library/src/main/java/com/example/lib/Library.java b/unified-prototype/unified-plugin/plugin-jvm/src/main/resources/templates/java-library/src/main/java/com/example/lib/Library.java new file mode 100644 index 00000000..b6d22b4f --- /dev/null +++ b/unified-prototype/unified-plugin/plugin-jvm/src/main/resources/templates/java-library/src/main/java/com/example/lib/Library.java @@ -0,0 +1,13 @@ +package com.example.lib; + +import com.google.common.collect.ImmutableList; + +public class Library { + public Iterable getMessages() { + // Verify that Guava is available + ImmutableList.Builder builder = ImmutableList.builder(); + builder.add("Hello from Java " + System.getProperty("java.version")); + + return builder.build(); + } +}