diff --git a/gradle-plugins/src/integTest/groovy/com/linkedin/pegasus/gradle/PegasusPluginIntegrationTest.groovy b/gradle-plugins/src/integTest/groovy/com/linkedin/pegasus/gradle/PegasusPluginIntegrationTest.groovy index 0fea14908d..4e7912326b 100644 --- a/gradle-plugins/src/integTest/groovy/com/linkedin/pegasus/gradle/PegasusPluginIntegrationTest.groovy +++ b/gradle-plugins/src/integTest/groovy/com/linkedin/pegasus/gradle/PegasusPluginIntegrationTest.groovy @@ -22,7 +22,7 @@ class PegasusPluginIntegrationTest extends Specification { .withProjectDir(tempDir.root) .withPluginClasspath() .withArguments('mainDataTemplateJar') - .forwardOutput() + //.forwardOutput() .build() then: diff --git a/gradle-plugins/src/integTest/groovy/com/linkedin/pegasus/gradle/publishing/PegasusPluginIvyPublishIntegrationTest.groovy b/gradle-plugins/src/integTest/groovy/com/linkedin/pegasus/gradle/publishing/PegasusPluginIvyPublishIntegrationTest.groovy index c1f57185bb..19978be45b 100644 --- a/gradle-plugins/src/integTest/groovy/com/linkedin/pegasus/gradle/publishing/PegasusPluginIvyPublishIntegrationTest.groovy +++ b/gradle-plugins/src/integTest/groovy/com/linkedin/pegasus/gradle/publishing/PegasusPluginIvyPublishIntegrationTest.groovy @@ -28,7 +28,17 @@ class PegasusPluginIvyPublishIntegrationTest extends Specification { localIvyRepo = localRepo.newFolder('local-ivy-repo').toURI().toURL() } - def 'publishes and consumes dataTemplate configurations'() { + /** + * Regression test illustrating how to consume software components published using the modern Ivy publishing mechanism, + * augmented by Gradle Module Metadata. + * + *

By requesting a named capability instead of a specific configuration name, we can consume pegasus + * artifacts in a forward-compatible manner. No special rules are required for the consumer. + + *

For more about Gradle Module Metadata, see Understanding Gradle Module Metadata. + * + */ + def 'publishes and consumes dataTemplate configurations with Gradle Module Metadata'() { given: def gradlePropertiesFile = grandparentProject.newFile('gradle.properties') gradlePropertiesFile << ''' @@ -54,8 +64,6 @@ class PegasusPluginIvyPublishIntegrationTest extends Specification { | pegasusPlugin files(${System.getProperty('integTest.pegasusPluginDependencies')}) |} | - |//tasks.withType(GenerateModuleMetadata) { enabled=false } - | |//modern ivy-publish configuration |publishing { | publications { @@ -85,7 +93,7 @@ class PegasusPluginIvyPublishIntegrationTest extends Specification { .withProjectDir(grandparentProject.root) .withPluginClasspath() .withArguments('publish', '-is') //uploadDataTemplate - .forwardOutput() + //.forwardOutput() //.withDebug(true) def grandparentResult = grandparentRunner.build() @@ -135,7 +143,6 @@ class PegasusPluginIvyPublishIntegrationTest extends Specification { | dataTemplateCompile files(${System.getProperty('integTest.dataTemplateCompileDependencies')}) | pegasusPlugin files(${System.getProperty('integTest.pegasusPluginDependencies')}) | - | //dataModel group: 'com.linkedin.pegasus-grandparent-demo', name: 'grandparent', version: '1.0.0', configuration: 'dataTemplate' | dataModel ('com.linkedin.pegasus-grandparent-demo:grandparent:1.0.0') { | capabilities { | requireCapability('com.linkedin.pegasus-grandparent-demo:grandparent-data-template:1.0.0') // TODO Gradle 6.0 requires an explicit version, 6.? does not @@ -143,8 +150,6 @@ class PegasusPluginIvyPublishIntegrationTest extends Specification { | } |} | - |//tasks.withType(GenerateModuleMetadata) { enabled=false } - | |//modern ivy-publish configuration |publishing { | publications { @@ -175,7 +180,7 @@ class PegasusPluginIvyPublishIntegrationTest extends Specification { .withProjectDir(parentProject.root) .withPluginClasspath() .withArguments('publish', '-is') - .forwardOutput() + //.forwardOutput() //.withDebug(true) def parentResult = parentRunner.build() @@ -225,7 +230,6 @@ class PegasusPluginIvyPublishIntegrationTest extends Specification { | dataTemplateCompile files(${System.getProperty('integTest.dataTemplateCompileDependencies')}) | pegasusPlugin files(${System.getProperty('integTest.pegasusPluginDependencies')}) | - | //dataModel group: 'com.linkedin.pegasus-parent-demo', name: 'parent', version: '1.0.0', configuration: 'dataTemplate' | dataModel ('com.linkedin.pegasus-parent-demo:parent:1.0.0') { | capabilities { | requireCapability('com.linkedin.pegasus-parent-demo:parent-data-template:1.0.0') // TODO Gradle 6.0 requires an explicit version, 6.? does not @@ -233,8 +237,6 @@ class PegasusPluginIvyPublishIntegrationTest extends Specification { | } |} | - |//tasks.withType(GenerateModuleMetadata) { enabled=false } - | |//modern ivy-publish configuration |publishing { | publications { @@ -269,8 +271,287 @@ class PegasusPluginIvyPublishIntegrationTest extends Specification { .withProjectDir(childProject.root) .withPluginClasspath() .withArguments('publish', '-is') //uploadDataTemplate - .forwardOutput() - .withDebug(true) + //.forwardOutput() + //.withDebug(true) + + def childResult = childRunner.build() + + then: + childResult.task(':compileMainGeneratedDataTemplateJava').outcome == TaskOutcome.SUCCESS + childResult.task(':generateDescriptorFileForIvyPublication').outcome == TaskOutcome.SUCCESS + + def childProjectIvyDescriptor = new File(localIvyRepo.path, 'com.linkedin.pegasus-child-demo/child/1.0.0/ivy-1.0.0.xml') + childProjectIvyDescriptor.exists() + def childProjectIvyDescriptorContents = childProjectIvyDescriptor.text + def expectedChildContents = new File(Thread.currentThread().contextClassLoader.getResource('ivy/modern/expectedChildIvyDescriptorContents.txt').toURI()).text + childProjectIvyDescriptorContents.contains expectedChildContents + + def childProjectPrimaryArtifact = new File(localIvyRepo.path, 'com.linkedin.pegasus-child-demo/child/1.0.0/child-1.0.0.jar') + childProjectPrimaryArtifact.exists() + //NB note naming scheme of data-template jar changes when classifier, not appendix, is used + def childProjectDataTemplateArtifact = new File(localIvyRepo.path, 'com.linkedin.pegasus-child-demo/child/1.0.0/child-1.0.0-data-template.jar') + childProjectDataTemplateArtifact.exists() + + assertZipContains(childProjectDataTemplateArtifact, 'com/linkedin/child/Photo.class') + assertZipContains(childProjectDataTemplateArtifact, 'pegasus/com/linkedin/child/Photo.pdl') + } + + /** + * Regression test illustrating how to consume software components published using the modern Ivy format. + * + *

By requesting a named capability instead of a specific configuration name, we can consume pegasus + * artifacts in a forward-compatible manner. + * + * Note that, in order to derive information about the capabilities of a software component, we must augment + * the consumer logic with a ComponentMetadataRule. + * + *

Unlike the above test, Gradle Module Metadata is not published or consumed. + */ + def 'publishes and consumes dataTemplate configurations without Gradle Module Metadata'() { + given: + def gradlePropertiesFile = grandparentProject.newFile('gradle.properties') + gradlePropertiesFile << ''' + |group=com.linkedin.pegasus-grandparent-demo + |version=1.0.0 + |'''.stripMargin() + + def settingsFile = grandparentProject.newFile('settings.gradle') + settingsFile << "rootProject.name = 'grandparent'" + + grandparentProject.newFile('build.gradle') << """ + |plugins { + | id 'ivy-publish' + | id 'pegasus' + |} + | + |repositories { + | mavenCentral() + |} + | + |dependencies { + | dataTemplateCompile files(${System.getProperty('integTest.dataTemplateCompileDependencies')}) + | pegasusPlugin files(${System.getProperty('integTest.pegasusPluginDependencies')}) + |} + | + |tasks.withType(GenerateModuleMetadata) { enabled=false } + | + |//modern ivy-publish configuration + |publishing { + | publications { + | ivy(IvyPublication) { + | from components.java + | } + | } + | repositories { + | ivy { url '$localIvyRepo' } + | } + |} + """.stripMargin() + + // Create a simple pdl schema, borrowed from restli-example-api + def schemaFilename = 'LatLong.pdl' + def grandparentPegasusDir = grandparentProject.newFolder('src', 'main', 'pegasus', 'com', 'linkedin', 'grandparent') + def grandparentPdlFile = new File("$grandparentPegasusDir.path$File.separator$schemaFilename") + grandparentPdlFile << '''namespace com.linkedin.grandparent + | + |record LatLong { + | latitude: optional float + | longitude: optional float + |}'''.stripMargin() + + when: + def grandparentRunner = GradleRunner.create() + .withProjectDir(grandparentProject.root) + .withPluginClasspath() + .withArguments('publish', '-is') //uploadDataTemplate + //.forwardOutput() + //.withDebug(true) + + def grandparentResult = grandparentRunner.build() + + then: + grandparentResult.task(':compileMainGeneratedDataTemplateJava').outcome == TaskOutcome.SUCCESS + grandparentResult.task(':generateDescriptorFileForIvyPublication').outcome == TaskOutcome.SUCCESS + + def grandparentProjectIvyDescriptor = new File(localIvyRepo.path, 'com.linkedin.pegasus-grandparent-demo/grandparent/1.0.0/ivy-1.0.0.xml') + grandparentProjectIvyDescriptor.exists() + def grandparentProjectIvyDescriptorContents = grandparentProjectIvyDescriptor.text + def expectedGrandparentContents = new File(Thread.currentThread().contextClassLoader.getResource('ivy/modern/expectedGrandparentIvyDescriptorContents.txt').toURI()).text + grandparentProjectIvyDescriptorContents.contains expectedGrandparentContents + + def grandparentProjectPrimaryArtifact = new File(localIvyRepo.path, 'com.linkedin.pegasus-grandparent-demo/grandparent/1.0.0/grandparent-1.0.0.jar') + grandparentProjectPrimaryArtifact.exists() + //NB note naming scheme of data-template jar changes when classifier, not appendix, is used + def grandparentProjectDataTemplateArtifact = new File(localIvyRepo.path, 'com.linkedin.pegasus-grandparent-demo/grandparent/1.0.0/grandparent-1.0.0-data-template.jar') + grandparentProjectDataTemplateArtifact.exists() + + assertZipContains(grandparentProjectDataTemplateArtifact, 'com/linkedin/grandparent/LatLong.class') + assertZipContains(grandparentProjectDataTemplateArtifact, 'pegasus/com/linkedin/grandparent/LatLong.pdl') + + when: 'a parent project consumes the grandparent project data-template jar' + + gradlePropertiesFile = parentProject.newFile('gradle.properties') + gradlePropertiesFile << ''' + |group=com.linkedin.pegasus-parent-demo + |version=1.0.0 + |'''.stripMargin() + + settingsFile = parentProject.newFile('settings.gradle') + settingsFile << "rootProject.name = 'parent'" + + parentProject.newFile('build.gradle') << """ + |plugins { + | id 'ivy-publish' + | id 'pegasus' + |} + | + |repositories { + | ivy { url '$localIvyRepo' } + | mavenCentral() + |} + | + |dependencies { + | dataTemplateCompile files(${System.getProperty('integTest.dataTemplateCompileDependencies')}) + | pegasusPlugin files(${System.getProperty('integTest.pegasusPluginDependencies')}) + | + | dataModel ('com.linkedin.pegasus-grandparent-demo:grandparent:1.0.0') { + | capabilities { + | requireCapability('com.linkedin.pegasus-grandparent-demo:grandparent-data-template:1.0.0') // TODO Gradle 6.0 requires an explicit version, 6.? does not + | } + | } + | + | components.all(com.linkedin.pegasus.gradle.rules.PegasusIvyVariantDerivationRule) + |} + | + |tasks.withType(GenerateModuleMetadata) { enabled=false } + | + |//modern ivy-publish configuration + |publishing { + | publications { + | ivy(IvyPublication) { + | from components.java + | } + | } + | repositories { + | ivy { url '$localIvyRepo' } + | } + |} + """.stripMargin() + + // Create a simple pdl schema which references a grandparent type + schemaFilename = 'EXIF.pdl' + def parentPegasusDir = parentProject.newFolder('src', 'main', 'pegasus', 'com', 'linkedin', 'parent') + def parentPdlFile = new File("$parentPegasusDir.path$File.separator$schemaFilename") + parentPdlFile << '''namespace com.linkedin.parent + | + |import com.linkedin.grandparent.LatLong + | + |record EXIF { + | isFlash: optional boolean = true + | location: optional LatLong + |}'''.stripMargin() + + def parentRunner = GradleRunner.create() + .withProjectDir(parentProject.root) + .withPluginClasspath() + .withArguments('publish', '-is') + //.forwardOutput() + //.withDebug(true) + + def parentResult = parentRunner.build() + + then: + parentResult.task(':compileMainGeneratedDataTemplateJava').outcome == TaskOutcome.SUCCESS + parentResult.task(':generateDescriptorFileForIvyPublication').outcome == TaskOutcome.SUCCESS + + def parentProjectIvyDescriptor = new File(localIvyRepo.path, 'com.linkedin.pegasus-parent-demo/parent/1.0.0/ivy-1.0.0.xml') + parentProjectIvyDescriptor.exists() + def parentProjectIvyDescriptorContents = parentProjectIvyDescriptor.text + def expectedParentContents = new File(Thread.currentThread().contextClassLoader.getResource('ivy/modern/expectedParentIvyDescriptorContents.txt').toURI()).text + parentProjectIvyDescriptorContents.contains expectedParentContents + + def parentProjectPrimaryArtifact = new File(localIvyRepo.path, 'com.linkedin.pegasus-parent-demo/parent/1.0.0/parent-1.0.0.jar') + parentProjectPrimaryArtifact.exists() + //NB note naming scheme of data-template jar changes when classifier, not appendix, is used + def parentProjectDataTemplateArtifact = new File(localIvyRepo.path, 'com.linkedin.pegasus-parent-demo/parent/1.0.0/parent-1.0.0-data-template.jar') + parentProjectDataTemplateArtifact.exists() + + assertZipContains(parentProjectDataTemplateArtifact, 'com/linkedin/parent/EXIF.class') + assertZipContains(parentProjectDataTemplateArtifact, 'pegasus/com/linkedin/parent/EXIF.pdl') + + when: 'a child project transitively consumes the grandparent project data-template jar' + + gradlePropertiesFile = childProject.newFile('gradle.properties') + gradlePropertiesFile << ''' + |group=com.linkedin.pegasus-child-demo + |version=1.0.0 + |'''.stripMargin() + + settingsFile = childProject.newFile('settings.gradle') + settingsFile << "rootProject.name = 'child'" + + childProject.newFile('build.gradle') << """ + |plugins { + | id 'ivy-publish' + | id 'pegasus' + |} + | + |repositories { + | ivy { url '$localIvyRepo' } + | mavenCentral() + |} + | + |dependencies { + | dataTemplateCompile files(${System.getProperty('integTest.dataTemplateCompileDependencies')}) + | pegasusPlugin files(${System.getProperty('integTest.pegasusPluginDependencies')}) + | + | dataModel ('com.linkedin.pegasus-parent-demo:parent:1.0.0') { + | capabilities { + | requireCapability('com.linkedin.pegasus-parent-demo:parent-data-template:1.0.0') // TODO Gradle 6.0 requires an explicit version, 6.? does not + | } + | } + | + | components.all(com.linkedin.pegasus.gradle.rules.PegasusIvyVariantDerivationRule) + |} + | + |tasks.withType(GenerateModuleMetadata) { enabled=false } + | + |//modern ivy-publish configuration + |publishing { + | publications { + | ivy(IvyPublication) { + | from components.java + | } + | } + | repositories { + | ivy { url '$localIvyRepo' } + | } + |} + |""".stripMargin() + + // Create a simple pdl schema which references parent and grandparent types + schemaFilename = 'Photo.pdl' + def childPegasusDir = childProject.newFolder('src', 'main', 'pegasus', 'com', 'linkedin', 'child') + def childPdlFile = new File("$childPegasusDir.path$File.separator$schemaFilename") + childPdlFile << '''namespace com.linkedin.child + | + |import com.linkedin.grandparent.LatLong + |import com.linkedin.parent.EXIF + | + |record Photo { + | id: long + | urn: string + | title: string + | exif: EXIF + | backupLocation: optional LatLong + |}'''.stripMargin() + + def childRunner = GradleRunner.create() + .withProjectDir(childProject.root) + .withPluginClasspath() + .withArguments('publish', '-is') + //.forwardOutput() + //.withDebug(true) def childResult = childRunner.build() @@ -298,4 +579,4 @@ class PegasusPluginIvyPublishIntegrationTest extends Specification { return new ZipFile(zip).getEntry(path) } -} +} \ No newline at end of file diff --git a/gradle-plugins/src/integTest/groovy/com/linkedin/pegasus/gradle/publishing/PegasusPluginLegacyIvyPublishIntegrationTest.groovy b/gradle-plugins/src/integTest/groovy/com/linkedin/pegasus/gradle/publishing/PegasusPluginLegacyIvyPublishIntegrationTest.groovy index fdfbd8890d..1dc2694ee3 100644 --- a/gradle-plugins/src/integTest/groovy/com/linkedin/pegasus/gradle/publishing/PegasusPluginLegacyIvyPublishIntegrationTest.groovy +++ b/gradle-plugins/src/integTest/groovy/com/linkedin/pegasus/gradle/publishing/PegasusPluginLegacyIvyPublishIntegrationTest.groovy @@ -263,6 +263,258 @@ class PegasusPluginLegacyIvyPublishIntegrationTest extends Specification { assertZipContains(childProjectDataTemplateArtifact, 'pegasus/com/linkedin/child/Photo.pdl') } + /** + * Regression test illustrating how to consume software components published using the legacy Ivy format. + * + *

By requesting a named capability instead of a specific configuration name, we can consume pegasus + * artifacts in a forward-compatible manner. + * + * Note that, in order to derive information about the capabilities of a software component, we must augment + * the consumer logic with a ComponentMetadataRule. + * + *

See Modeling feature variants and optional dependencies + * and Consuming Feature Variants + * for more information about capabilities. + */ + def 'publishes with legacy ivies but derives capabilities from dataTemplate configurations'() { + given: + def gradlePropertiesFile = grandparentProject.newFile('gradle.properties') + gradlePropertiesFile << ''' + |group=com.linkedin.pegasus-grandparent-demo + |version=1.0.0 + |'''.stripMargin() + + def settingsFile = grandparentProject.newFile('settings.gradle') + settingsFile << "rootProject.name = 'grandparent'" + + grandparentProject.newFile('build.gradle') << """ + |plugins { + | id 'pegasus' + |} + | + |repositories { + | mavenCentral() + |} + | + |dependencies { + | dataTemplateCompile files(${System.getProperty('integTest.dataTemplateCompileDependencies')}) + | pegasusPlugin files(${System.getProperty('integTest.pegasusPluginDependencies')}) + |} + | + |//legacy publishing configuration + |tasks.withType(Upload) { + | repositories { + | ivy { url '$localIvyRepo' } + | } + |}""".stripMargin() + + // Create a simple pdl schema, borrowed from restli-example-api + def schemaFilename = 'LatLong.pdl' + def grandparentPegasusDir = grandparentProject.newFolder('src', 'main', 'pegasus', 'com', 'linkedin', 'grandparent') + def grandparentPdlFile = new File("$grandparentPegasusDir.path$File.separator$schemaFilename") + grandparentPdlFile << '''namespace com.linkedin.grandparent + | + |record LatLong { + | latitude: optional float + | longitude: optional float + |}'''.stripMargin() + + when: + def grandparentRunner = GradleRunner.create() + .withProjectDir(grandparentProject.root) + .withPluginClasspath() + .withArguments('uploadDataTemplate', 'uploadTestDataTemplate', 'uploadAvroSchema', 'uploadTestAvroSchema', 'uploadArchives', '-is') + //.forwardOutput() + //.withDebug(true) + + def grandparentResult = grandparentRunner.build() + + then: + grandparentResult.task(':compileMainGeneratedDataTemplateJava').outcome == TaskOutcome.SUCCESS + grandparentResult.task(':uploadDataTemplate').outcome == TaskOutcome.SUCCESS + grandparentResult.task(':uploadArchives').outcome == TaskOutcome.SUCCESS + + def grandparentProjectIvyDescriptor = new File(localIvyRepo.path, 'com.linkedin.pegasus-grandparent-demo/grandparent/1.0.0/ivy-1.0.0.xml') + grandparentProjectIvyDescriptor.exists() + def grandparentProjectIvyDescriptorContents = grandparentProjectIvyDescriptor.text + def expectedGrandparentContents = new File(Thread.currentThread().contextClassLoader.getResource('ivy/legacyWithVariantDerivation/expectedGrandparentIvyDescriptorContents.txt').toURI()).text + grandparentProjectIvyDescriptorContents.contains expectedGrandparentContents + + def grandparentProjectPrimaryArtifact = new File(localIvyRepo.path, 'com.linkedin.pegasus-grandparent-demo/grandparent/1.0.0/grandparent-1.0.0.jar') + grandparentProjectPrimaryArtifact.exists() + def grandparentProjectDataTemplateArtifact = new File(localIvyRepo.path, 'com.linkedin.pegasus-grandparent-demo/grandparent/1.0.0/grandparent-data-template-1.0.0.jar') + grandparentProjectDataTemplateArtifact.exists() + + assertZipContains(grandparentProjectDataTemplateArtifact, 'com/linkedin/grandparent/LatLong.class') + assertZipContains(grandparentProjectDataTemplateArtifact, 'pegasus/com/linkedin/grandparent/LatLong.pdl') + + when: 'a parent project consumes the grandparent project data-template jar' + + gradlePropertiesFile = parentProject.newFile('gradle.properties') + gradlePropertiesFile << ''' + |group=com.linkedin.pegasus-parent-demo + |version=1.0.0 + |'''.stripMargin() + + settingsFile = parentProject.newFile('settings.gradle') + settingsFile << "rootProject.name = 'parent'" + + parentProject.newFile('build.gradle') << """ + |plugins { + | id 'pegasus' + |} + | + |repositories { + | ivy { url '$localIvyRepo' } + | mavenCentral() + |} + | + |dependencies { + | dataTemplateCompile files(${System.getProperty('integTest.dataTemplateCompileDependencies')}) + | pegasusPlugin files(${System.getProperty('integTest.pegasusPluginDependencies')}) + | + | dataModel ('com.linkedin.pegasus-grandparent-demo:grandparent:1.0.0') { + | capabilities { + | requireCapability('com.linkedin.pegasus-grandparent-demo:grandparent-data-template:1.0.0') + | } + | } + | components.all(com.linkedin.pegasus.gradle.rules.PegasusIvyVariantDerivationRule) + |} + | + |//legacy publishing configuration + |tasks.withType(Upload) { + | repositories { + | ivy { url '$localIvyRepo' } + | } + |}""".stripMargin() + + // Create a simple pdl schema which references a grandparent type + schemaFilename = 'EXIF.pdl' + def parentPegasusDir = parentProject.newFolder('src', 'main', 'pegasus', 'com', 'linkedin', 'parent') + def parentPdlFile = new File("$parentPegasusDir.path$File.separator$schemaFilename") + parentPdlFile << '''namespace com.linkedin.parent + | + |import com.linkedin.grandparent.LatLong + | + |record EXIF { + | isFlash: optional boolean = true + | location: optional LatLong + |}'''.stripMargin() + + def parentRunner = GradleRunner.create() + .withProjectDir(parentProject.root) + .withPluginClasspath() + .withArguments('uploadDataTemplate', 'uploadTestDataTemplate', 'uploadAvroSchema', 'uploadTestAvroSchema', 'uploadArchives', '-is') + //.forwardOutput() + //.withDebug(true) + + def parentResult = parentRunner.build() + + then: + parentResult.task(':compileMainGeneratedDataTemplateJava').outcome == TaskOutcome.SUCCESS + parentResult.task(':uploadDataTemplate').outcome == TaskOutcome.SUCCESS + parentResult.task(':uploadArchives').outcome == TaskOutcome.SUCCESS + + def parentProjectIvyDescriptor = new File(localIvyRepo.path, 'com.linkedin.pegasus-parent-demo/parent/1.0.0/ivy-1.0.0.xml') + parentProjectIvyDescriptor.exists() + def parentProjectIvyDescriptorContents = parentProjectIvyDescriptor.text + def expectedParentContents = new File(Thread.currentThread().contextClassLoader.getResource('ivy/legacyWithVariantDerivation/expectedParentIvyDescriptorContents.txt').toURI()).text + parentProjectIvyDescriptorContents.contains expectedParentContents + + def parentProjectPrimaryArtifact = new File(localIvyRepo.path, 'com.linkedin.pegasus-parent-demo/parent/1.0.0/parent-1.0.0.jar') + parentProjectPrimaryArtifact.exists() + def parentProjectDataTemplateArtifact = new File(localIvyRepo.path, 'com.linkedin.pegasus-parent-demo/parent/1.0.0/parent-data-template-1.0.0.jar') + parentProjectDataTemplateArtifact.exists() + + assertZipContains(parentProjectDataTemplateArtifact, 'com/linkedin/parent/EXIF.class') + assertZipContains(parentProjectDataTemplateArtifact, 'pegasus/com/linkedin/parent/EXIF.pdl') + + when: 'a child project transitively consumes the grandparent project data-template jar' + + gradlePropertiesFile = childProject.newFile('gradle.properties') + gradlePropertiesFile << ''' + |group=com.linkedin.pegasus-child-demo + |version=1.0.0 + |'''.stripMargin() + + settingsFile = childProject.newFile('settings.gradle') + settingsFile << "rootProject.name = 'child'" + + childProject.newFile('build.gradle') << """ + |plugins { + | id 'pegasus' + |} + | + |repositories { + | ivy { url '$localIvyRepo' } + | mavenCentral() + |} + | + |dependencies { + | dataTemplateCompile files(${System.getProperty('integTest.dataTemplateCompileDependencies')}) + | pegasusPlugin files(${System.getProperty('integTest.pegasusPluginDependencies')}) + | + | dataModel ('com.linkedin.pegasus-parent-demo:parent:1.0.0') { + | capabilities { + | requireCapability('com.linkedin.pegasus-parent-demo:parent-data-template:1.0.0') + | } + | } + | components.all(com.linkedin.pegasus.gradle.rules.PegasusIvyVariantDerivationRule) + |} + | + |//legacy publishing configuration + |tasks.withType(Upload) { + | repositories { + | ivy { url '$localIvyRepo' } + | } + |}""".stripMargin() + + // Create a simple pdl schema which references parent and grandparent types + schemaFilename = 'Photo.pdl' + def childPegasusDir = childProject.newFolder('src', 'main', 'pegasus', 'com', 'linkedin', 'child') + def childPdlFile = new File("$childPegasusDir.path$File.separator$schemaFilename") + childPdlFile << '''namespace com.linkedin.child + | + |import com.linkedin.grandparent.LatLong + |import com.linkedin.parent.EXIF + | + |record Photo { + | id: long + | urn: string + | title: string + | exif: EXIF + | backupLocation: optional LatLong + |}'''.stripMargin() + + def childRunner = GradleRunner.create() + .withProjectDir(childProject.root) + .withPluginClasspath() + .withArguments('uploadDataTemplate', 'uploadTestDataTemplate', 'uploadAvroSchema', 'uploadTestAvroSchema', 'uploadArchives', '-is') + //.forwardOutput() + //.withDebug(true) + + def childResult = childRunner.build() + + then: + childResult.task(':compileMainGeneratedDataTemplateJava').outcome == TaskOutcome.SUCCESS + childResult.task(':uploadDataTemplate').outcome == TaskOutcome.SUCCESS + childResult.task(':uploadArchives').outcome == TaskOutcome.SUCCESS + + def childProjectIvyDescriptor = new File(localIvyRepo.path, 'com.linkedin.pegasus-child-demo/child/1.0.0/ivy-1.0.0.xml') + childProjectIvyDescriptor.exists() + def childProjectIvyDescriptorContents = childProjectIvyDescriptor.text + def expectedChildContents = new File(Thread.currentThread().contextClassLoader.getResource('ivy/legacyWithVariantDerivation/expectedChildIvyDescriptorContents.txt').toURI()).text + childProjectIvyDescriptorContents.contains expectedChildContents + + def childProjectPrimaryArtifact = new File(localIvyRepo.path, 'com.linkedin.pegasus-child-demo/child/1.0.0/child-1.0.0.jar') + childProjectPrimaryArtifact.exists() + def childProjectDataTemplateArtifact = new File(localIvyRepo.path, 'com.linkedin.pegasus-child-demo/child/1.0.0/child-data-template-1.0.0.jar') + childProjectDataTemplateArtifact.exists() + + assertZipContains(childProjectDataTemplateArtifact, 'com/linkedin/child/Photo.class') + assertZipContains(childProjectDataTemplateArtifact, 'pegasus/com/linkedin/child/Photo.pdl') + } + private static boolean assertZipContains(File zip, String path) { return new ZipFile(zip).getEntry(path) } diff --git a/gradle-plugins/src/integTest/resources/ivy/legacyWithVariantDerivation/expectedChildIvyDescriptorContents.txt b/gradle-plugins/src/integTest/resources/ivy/legacyWithVariantDerivation/expectedChildIvyDescriptorContents.txt new file mode 100644 index 0000000000..a3be89ecf8 --- /dev/null +++ b/gradle-plugins/src/integTest/resources/ivy/legacyWithVariantDerivation/expectedChildIvyDescriptorContents.txt @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gradle-plugins/src/integTest/resources/ivy/legacyWithVariantDerivation/expectedGrandparentIvyDescriptorContents.txt b/gradle-plugins/src/integTest/resources/ivy/legacyWithVariantDerivation/expectedGrandparentIvyDescriptorContents.txt new file mode 100644 index 0000000000..7c702057b8 --- /dev/null +++ b/gradle-plugins/src/integTest/resources/ivy/legacyWithVariantDerivation/expectedGrandparentIvyDescriptorContents.txt @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gradle-plugins/src/integTest/resources/ivy/legacyWithVariantDerivation/expectedParentIvyDescriptorContents.txt b/gradle-plugins/src/integTest/resources/ivy/legacyWithVariantDerivation/expectedParentIvyDescriptorContents.txt new file mode 100644 index 0000000000..998b2e12da --- /dev/null +++ b/gradle-plugins/src/integTest/resources/ivy/legacyWithVariantDerivation/expectedParentIvyDescriptorContents.txt @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gradle-plugins/src/integTest/resources/ivy/modern/expectedChildIvyDescriptorContents.txt b/gradle-plugins/src/integTest/resources/ivy/modern/expectedChildIvyDescriptorContents.txt index 4e1bae377d..84fe67e35b 100644 --- a/gradle-plugins/src/integTest/resources/ivy/modern/expectedChildIvyDescriptorContents.txt +++ b/gradle-plugins/src/integTest/resources/ivy/modern/expectedChildIvyDescriptorContents.txt @@ -4,10 +4,13 @@ + + + @@ -18,4 +21,8 @@ + + + + \ No newline at end of file diff --git a/gradle-plugins/src/integTest/resources/ivy/modern/expectedGrandparentIvyDescriptorContents.txt b/gradle-plugins/src/integTest/resources/ivy/modern/expectedGrandparentIvyDescriptorContents.txt index 1260d4b919..3e08b5d666 100644 --- a/gradle-plugins/src/integTest/resources/ivy/modern/expectedGrandparentIvyDescriptorContents.txt +++ b/gradle-plugins/src/integTest/resources/ivy/modern/expectedGrandparentIvyDescriptorContents.txt @@ -4,14 +4,19 @@ + + + + + \ No newline at end of file diff --git a/gradle-plugins/src/integTest/resources/ivy/modern/expectedParentIvyDescriptorContents.txt b/gradle-plugins/src/integTest/resources/ivy/modern/expectedParentIvyDescriptorContents.txt index 3703f236de..c07235829c 100644 --- a/gradle-plugins/src/integTest/resources/ivy/modern/expectedParentIvyDescriptorContents.txt +++ b/gradle-plugins/src/integTest/resources/ivy/modern/expectedParentIvyDescriptorContents.txt @@ -4,10 +4,13 @@ + + + @@ -18,4 +21,8 @@ + + + + \ No newline at end of file diff --git a/gradle-plugins/src/main/java/com/linkedin/pegasus/gradle/PegasusPlugin.java b/gradle-plugins/src/main/java/com/linkedin/pegasus/gradle/PegasusPlugin.java index 126a640577..898ef34393 100644 --- a/gradle-plugins/src/main/java/com/linkedin/pegasus/gradle/PegasusPlugin.java +++ b/gradle-plugins/src/main/java/com/linkedin/pegasus/gradle/PegasusPlugin.java @@ -64,6 +64,7 @@ import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPluginConvention; import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.publish.ivy.plugins.IvyPublishPlugin; import org.gradle.api.tasks.Copy; import org.gradle.api.tasks.Delete; import org.gradle.api.tasks.SourceSet; @@ -1830,30 +1831,35 @@ protected GenerateDataTemplateTask configureDataTemplateGeneration(Project proje // FIXME change to #getArchiveFile(); breaks backwards-compatibility before 5.1 project.getDependencies().add(compileConfigName, project.files(dataTemplateJarTask.getArchivePath())); - project.getPlugins().withId("ivy-publish", ivyPublish -> { - // TODO if !atLeastGradle53 throw + project.getPlugins().withType(IvyPublishPlugin.class, ivyPublish -> { + if (!isAtLeastGradle61()) { + throw new GradleException("Using the ivy-publish plugin with the pegasus plugin requires Gradle 6.1 or higher " + + "at build time. Please upgrade."); + } JavaPluginExtension java = project.getExtensions().getByType(JavaPluginExtension.class); - if ("mainGeneratedDataTemplate".equals(targetSourceSet.getName())) { - // create new capability; automatically creates api and implementation configurations - java.registerFeature("dataTemplate", featureSpec -> { - featureSpec.usingSourceSet(targetSourceSet); - }); + // create new capabilities per source set; automatically creates api and implementation configurations + String featureName = mapSourceSetToFeatureName(targetSourceSet); + java.registerFeature(featureName, featureSpec -> { + featureSpec.usingSourceSet(targetSourceSet); + }); - // include pegasus files in the output of this SourceSet - TaskProvider processResources = project.getTasks().named(targetSourceSet.getProcessResourcesTaskName(), ProcessResources.class); - processResources.configure(it -> { - it.from(prepareSchemasForPublishTask, copy -> copy.into("pegasus")); - it.from(prepareLegacySchemasForPublishTask, copy -> copy.into(TRANSLATED_SCHEMAS_DIR)); - //TODO maybe add extensions - }); + // include pegasus files in the output of this SourceSet + TaskProvider processResources = project.getTasks().named(targetSourceSet.getProcessResourcesTaskName(), ProcessResources.class); + processResources.configure(it -> { + it.from(prepareSchemasForPublishTask, copy -> copy.into("pegasus")); + it.from(prepareLegacySchemasForPublishTask, copy -> copy.into(TRANSLATED_SCHEMAS_DIR)); + Sync copyExtensionSchemasTask = project.getTasks().withType(Sync.class).findByName(targetSourceSet.getName() + "CopyExtensionSchemas"); + if (copyExtensionSchemasTask != null) { + it.from(copyExtensionSchemasTask, copy -> copy.into("extensions")); + } + }); - // expose transitive dependencies to consumers via api configuration - Configuration mainGeneratedDataTemplateApi = project.getConfigurations().getByName(targetSourceSet.getApiConfigurationName()); - mainGeneratedDataTemplateApi.extendsFrom( - getDataModelConfig(project, sourceSet), - project.getConfigurations().getByName("dataTemplateCompile")); - } + // expose transitive dependencies to consumers via api configuration + Configuration mainGeneratedDataTemplateApi = project.getConfigurations().getByName(targetSourceSet.getApiConfigurationName()); + mainGeneratedDataTemplateApi.extendsFrom( + getDataModelConfig(project, targetSourceSet), + project.getConfigurations().getByName("dataTemplateCompile")); }); if (debug) @@ -1871,6 +1877,34 @@ protected GenerateDataTemplateTask configureDataTemplateGeneration(Project proje return generateDataTemplatesTask; } + private String mapSourceSetToFeatureName(SourceSet sourceSet) { + String featureName = ""; + switch (sourceSet.getName()) { + case "mainGeneratedDataTemplate": + featureName = "dataTemplate"; + break; + case "testGeneratedDataTemplate": + featureName = "testDataTemplate"; + break; + case "mainGeneratedRest": + featureName = "restClient"; + break; + case "testGeneratedRest": + featureName = "testRestClient"; + break; + case "mainGeneratedAvroSchema": + featureName = "avroSchema"; + break; + case "testGeneratedAvroSchema": + featureName = "testAvroSchema"; + break; + default: + String msg = String.format("Unable to map %s to an appropriate feature name", sourceSet); + throw new GradleException(msg); + } + return featureName; + } + // Generate rest client from idl files generated from java source files in the specified source set. // // This generates rest client source files from idl file generated from java source files @@ -2260,8 +2294,12 @@ private Task publishPegasusSchemaSnapshot(Project project, SourceSet sourceSet, task.onlyIf(t -> !SharedFileUtils.getSuffixedFiles(project, inputDir, PDL_FILE_SUFFIX).isEmpty()); }); } + protected static boolean isAtLeastGradle54() { return GradleVersion.current().getBaseVersion().compareTo(GradleVersion.version("5.4")) >= 0; } + protected static boolean isAtLeastGradle61() { + return GradleVersion.current().getBaseVersion().compareTo(GradleVersion.version("6.1")) >= 0; + } } diff --git a/gradle-plugins/src/main/java/com/linkedin/pegasus/gradle/rules/PegasusIvyVariantDerivationRule.java b/gradle-plugins/src/main/java/com/linkedin/pegasus/gradle/rules/PegasusIvyVariantDerivationRule.java new file mode 100644 index 0000000000..a6d7838fcc --- /dev/null +++ b/gradle-plugins/src/main/java/com/linkedin/pegasus/gradle/rules/PegasusIvyVariantDerivationRule.java @@ -0,0 +1,87 @@ +package com.linkedin.pegasus.gradle.rules; + +import org.gradle.api.artifacts.ComponentMetadataContext; +import org.gradle.api.artifacts.ComponentMetadataRule; +import org.gradle.api.artifacts.ModuleVersionIdentifier; +import org.gradle.api.artifacts.ivy.IvyModuleDescriptor; +import org.gradle.api.attributes.Category; +import org.gradle.api.attributes.LibraryElements; +import org.gradle.api.attributes.Usage; +import org.gradle.api.model.ObjectFactory; + +import javax.inject.Inject; + +/** + * Rule for deriving Gradle variants from a software component which publishes pegasus jars. + * + *

Instead of consuming the dataTemplate configuration directly, this rule adds a "-data-template" capability to the + * primary GAV coordinates of the component. + * + *

build.gradle usage example before this change: + *

+ *   configurations {
+ *    dataModel group: 'com.acme.foo', name: 'foo', version: '1.0.0', configuration: 'dataTemplate'
+ *   }
+ * 
+ * + * build.gradle usage example with this rule: + *
+ *   configurations {
+ *     dataModel ('com.acme.foo:foo:1.0.0') {
+ *       capabilities {
+ *         requireCapability('com.acme.foo:foo-data-template:1.0.0')
+ *       }
+ *     }
+ *     components.all(com.linkedin.pegasus.gradle.rules.PegasusIvyVariantDerivationRule)
+ *   }
+ * 
+ * + */ +public class PegasusIvyVariantDerivationRule implements ComponentMetadataRule { + + private final ObjectFactory objects; + + @Inject + public PegasusIvyVariantDerivationRule(ObjectFactory objects) { + this.objects = objects; + } + + @Override + public void execute(ComponentMetadataContext context) { + if (context.getDescriptor(IvyModuleDescriptor.class) == null) { + return; // this component's metadata is not Ivy-based; bail out + } + + // for backwards-compatibility with older Ivy descriptors, first try to derive a variant from the dataTemplate configuration + context.getDetails().maybeAddVariant("dataTemplateApiElements", "dataTemplate", variantMetadata -> { + variantMetadata.attributes(attributes -> { + attributes.attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.class, Category.LIBRARY)); + attributes.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.class, LibraryElements.JAR)); + attributes.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.class, Usage.JAVA_API)); + }); + ModuleVersionIdentifier id = context.getDetails().getId(); + variantMetadata.withCapabilities(capabilities -> capabilities.addCapability(id.getGroup(), id.getName() + "-data-template", id.getVersion())); + }); + + context.getDetails().maybeAddVariant("dataTemplateRuntimeElements", "mainGeneratedDataTemplateRuntimeElements", variantMetadata -> { + variantMetadata.attributes(attributes -> { + attributes.attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.class, Category.LIBRARY)); + attributes.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.class, LibraryElements.JAR)); + attributes.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.class, Usage.JAVA_RUNTIME)); + }); + ModuleVersionIdentifier id = context.getDetails().getId(); + variantMetadata.withCapabilities(capabilities -> capabilities.addCapability(id.getGroup(), id.getName() + "-data-template", id.getVersion())); + }); + + context.getDetails().maybeAddVariant("dataTemplateApiElements", "mainGeneratedDataTemplateApiElements", variantMetadata -> { + variantMetadata.attributes(attributes -> { + attributes.attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.class, Category.LIBRARY)); + attributes.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.class, LibraryElements.JAR)); + attributes.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.class, Usage.JAVA_API)); + }); + ModuleVersionIdentifier id = context.getDetails().getId(); + variantMetadata.withCapabilities(capabilities -> capabilities.addCapability(id.getGroup(), id.getName() + "-data-template", id.getVersion())); + }); + + } +}