Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ out
*.iml
*.iws
.idea

.claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import groovy.transform.CompileDynamic
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.ApplicationPlugin
import org.gradle.api.tasks.application.CreateStartScripts
import org.gradle.api.plugins.JavaApplication

/**
* Combine the nebula-ospackage-application with the nebula-ospackage-daemon plugin. As with the nebula-ospackage-application,
Expand All @@ -46,24 +46,23 @@ class OspackageApplicationDaemonPlugin implements Plugin<Project> {
project.plugins.apply(OspackageApplicationPlugin)
def ospackageApplicationExtension = project.extensions.getByType(OspackageApplicationExtension)

CreateStartScripts startScripts = (CreateStartScripts) project.tasks.getByName(ApplicationPlugin.TASK_START_SCRIPTS_NAME)

project.plugins.apply(OspackageDaemonPlugin)

// Mechanism for user to configure daemon further
List<Closure> daemonConfiguration = []
setApplicationDaemon(project, daemonConfiguration)

// TODO Convention mapping on definition instead of afterEvaluate
// Keep afterEvaluate to wait for user configuration of applicationName
project.afterEvaluate {
// TODO Sanitize name
def name = startScripts.applicationName
// Use strongly-typed application extension instead of eager task realization
JavaApplication appExtension = project.extensions.getByType(JavaApplication)
def name = appExtension.applicationName ?: project.name

// Add daemon to project
DaemonExtension daemonExt = project.extensions.getByType(DaemonExtension)
def definition = daemonExt.daemon { DaemonDefinition daemonDefinition ->
daemonDefinition.setDaemonName(name)
daemonDefinition.setCommand("${ospackageApplicationExtension.prefix}/${name}/bin/${name}".toString())
daemonDefinition.setCommand("${ospackageApplicationExtension.prefix.get()}/${name}/bin/${name}".toString())
}

daemonConfiguration.each { confClosure ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@

package com.netflix.gradle.plugins.application

class OspackageApplicationExtension {
String prefix
import org.gradle.api.provider.Property

String distribution
interface OspackageApplicationExtension {
Property<String> getPrefix()
Property<String> getDistribution()
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ package com.netflix.gradle.plugins.application

import com.netflix.gradle.plugins.packaging.ProjectPackagingExtension
import com.netflix.gradle.plugins.packaging.SystemPackagingPlugin
import groovy.transform.CompileDynamic
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.internal.IConventionAware
import org.gradle.api.distribution.DistributionContainer
import org.gradle.api.distribution.plugins.DistributionPlugin
import org.gradle.api.plugins.ApplicationPlugin
Expand All @@ -39,27 +39,28 @@ class OspackageApplicationPlugin implements Plugin<Project> {
OspackageApplicationExtension extension

@Override
@CompileDynamic
void apply(Project project) {
extension = project.extensions.create('ospackage_application', OspackageApplicationExtension)
def conventionMapping = ((IConventionAware) extension).conventionMapping
conventionMapping.map('prefix') { '/opt' }
conventionMapping.map('distribution') { '' }
extension.prefix.convention('/opt')
extension.distribution.convention('')

project.plugins.apply(ApplicationPlugin)
project.plugins.apply(SystemPackagingPlugin)

def distributions = project.getExtensions().getByType(DistributionContainer.class)
def mainDistribution = distributions.getByName(DistributionPlugin.MAIN_DISTRIBUTION_NAME)
def name = mainDistribution.getDistributionBaseName().map { baseName ->
def classifier = mainDistribution.getDistributionClassifier().getOrNull()
baseName + (classifier != null ? '-' + classifier : '')
def name = extension.prefix.map { prefix ->
String baseName = mainDistribution.getDistributionBaseName().get()
String classifier = mainDistribution.getDistributionClassifier().getOrNull()
return baseName + (classifier != null ? '-' + classifier : '')
}
def packaging = project.extensions.getByType(ProjectPackagingExtension)
def copyMain = project.copySpec() {
with(mainDistribution.contents)
into(name)
}
packaging.with(copyMain)
packaging.into(project.provider { extension.prefix })
packaging.into(extension.prefix.map { it })
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,16 @@ class OspackageApplicationSpringBootPlugin implements Plugin<Project> {
void apply(Project project) {
project.plugins.apply(OspackageApplicationPlugin)

// Validate Spring Boot plugin at configuration time using plugins.withId
// This automatically waits for plugin application
boolean springBootFound = false
project.plugins.withId("org.springframework.boot") {
springBootFound = true
}

// Only validate after other plugins have a chance to apply
project.afterEvaluate {
if (!project.plugins.hasPlugin('org.springframework.boot')) {
if (!springBootFound) {
project.logger.error("The '{}' plugin requires the '{}' plugin.",
"com.netflix.nebula.ospackage-application-spring-boot",
"org.springframework.boot")
Expand Down Expand Up @@ -116,9 +124,10 @@ class OspackageApplicationSpringBootPlugin implements Plugin<Project> {

// Workaround for https://github.com/gradle/gradle/issues/16371
if (GradleVersion.current().baseVersion >= GradleVersion.version('6.4').baseVersion) {
def mainClass = project.application.mainClass
project.tasks.named(ApplicationPlugin.TASK_START_SCRIPTS_NAME).configure {
doFirst {
if (!project.application.mainClass.isPresent()) {
if (!mainClass.isPresent()) {
throw new GradleException("mainClass should be configured in order to generate a valid start script. i.e. mainClass = 'com.netflix.app.MyApp'")
}
}
Expand Down Expand Up @@ -151,8 +160,10 @@ class OspackageApplicationSpringBootPlugin implements Plugin<Project> {
main {
contents {
into('lib') {
project.getConfigurations().getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME).files.findAll { file ->
file.getName() != project.tasks.getByName(JavaPlugin.JAR_TASK_NAME).outputs.files.singleFile.name
def jarTaskProvider = project.tasks.named(JavaPlugin.JAR_TASK_NAME)
def runtimeClasspath = project.configurations.named(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME)
runtimeClasspath.get().files.findAll { file ->
file.getName() != jarTaskProvider.get().outputs.files.singleFile.name
}.each { file ->
exclude file.name
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,49 +16,52 @@

package com.netflix.gradle.plugins.daemon

import org.gradle.api.internal.ConventionTask
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.MapProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputFiles
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import org.gradle.work.DisableCachingByDefault

/**
* Monster class that does everything.
*/
@DisableCachingByDefault
class DaemonTemplateTask extends ConventionTask {

DaemonTemplateTask() {
notCompatibleWithConfigurationCache("nebula.ospackage does not support configuration cache")
}
abstract class DaemonTemplateTask extends DefaultTask {

@Internal
Map<String, String> context
abstract MapProperty<String, Object> getContext()

@Internal
Collection<String> templates
abstract ListProperty<String> getTemplates()

@OutputDirectory
abstract DirectoryProperty getDestDir()

@Internal
File destDir
abstract Property<String> getTemplatesFolder()

@Internal
String templatesFolder
abstract Property<File> getProjectDirectory()

@TaskAction
def template() {
TemplateHelper templateHelper = new TemplateHelper(getDestDir(), getTemplatesFolder(), project)
getTemplates().collect { String templateName ->
templateHelper.generateFile(templateName, getContext())
}
DaemonTemplateTask() {
// Capture project directory during configuration
projectDirectory.convention(project.projectDir)
}

@Internal
Collection<File> getTemplatesOutput() {
return templates.collect {
new File(destDir, it)
@TaskAction
def template() {
TemplateHelper templateHelper = new TemplateHelper(
destDir.get().asFile,
templatesFolder.get(),
projectDirectory.get()
)
templates.get().collect { String templateName ->
templateHelper.generateFile(templateName, context.get())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import org.gradle.api.Project

class OspackageDaemonPlugin implements Plugin<Project> {
public static final String POST_INSTALL_TEMPLATE = "postInstall"
Project project
DaemonExtension extension
DaemonTemplatesConfigExtension daemonTemplatesConfigExtension
DefaultDaemonDefinitionExtension defaultDefinition
Expand All @@ -54,7 +53,6 @@ class OspackageDaemonPlugin implements Plugin<Project> {

@Override
void apply(Project project) {
this.project = project
project.plugins.apply(SystemPackagingBasePlugin)

DomainObjectSet<DaemonDefinition> daemonsList = WrapUtil.toDomainObjectSet(DaemonDefinition)
Expand Down Expand Up @@ -92,7 +90,7 @@ class OspackageDaemonPlugin implements Plugin<Project> {
String cleanedName = daemonName.replaceAll("\\W", "").capitalize()


File outputDir = new File(project.layout.buildDirectory.getAsFile().get(), "daemon/${cleanedName}/${task.name}")
def outputDirProvider = project.layout.buildDirectory.dir("daemon/${cleanedName}/${task.name}")

String defaultInitDScriptLocationTemplate = isRedhat ? "/etc/rc.d/init.d/\${daemonName}" : "/etc/init.d/\${daemonName}"
Map<String, String> templatesWithFileOutput = [
Expand All @@ -101,33 +99,54 @@ class OspackageDaemonPlugin implements Plugin<Project> {
'initd': defaultDefinition.initDScriptLocation ?: defaultInitDScriptLocationTemplate
]

DaemonTemplateTask templateTask = project.tasks.create("${task.name}${cleanedName}Daemon".toString(), DaemonTemplateTask)
templateTask.conventionMapping.map('destDir') { outputDir }
templateTask.conventionMapping.map('templatesFolder') { daemonTemplatesConfigExtension.folder ?: DEFAULT_TEMPLATES_FOLDER }
templateTask.conventionMapping.map('context') {
Map<String,Object> context = toContext(defaults, definition)
context.daemonName = daemonName
context.isRedhat = isRedhat
context.installCmd = definition.installCmd ?: LegacyInstallCmd.create(context)
context
def templateTaskProvider = project.tasks.register("${task.name}${cleanedName}Daemon", DaemonTemplateTask) {
// Use Property API instead of conventionMapping
it.destDir.convention(outputDirProvider)
it.templatesFolder.convention(daemonTemplatesConfigExtension.folder ?: DEFAULT_TEMPLATES_FOLDER)
it.context.convention(project.providers.provider {
Map<String,Object> context = toContext(defaults, definition)
context.daemonName = daemonName
context.isRedhat = isRedhat
context.installCmd = definition.installCmd ?: LegacyInstallCmd.create(context)
context
})
it.templates.convention(templatesWithFileOutput.keySet() + POST_INSTALL_TEMPLATE)
}
templateTask.conventionMapping.map('templates') { templatesWithFileOutput.keySet() + POST_INSTALL_TEMPLATE }

task.dependsOn(templateTask)
task.dependsOn(templateTaskProvider)
templatesWithFileOutput.each { String templateName, String destPathTemplate ->
File rendered = new File(outputDir, templateName) // To be created by task, ok that it's not around yet
String destPath = getDestPath(destPathTemplate, templateTask)
// Gradle CopySpec can't set the name of a file on the fly, we need to do a rename.
int slashIdx = destPath.lastIndexOf('/')
String destDir = destPath.substring(0,slashIdx)
String destFile = destPath.substring(slashIdx+1)
configureTask(task, rendered, destDir, destFile)
// Use lazy providers to avoid eager task realization
def renderedFileProvider = outputDirProvider.map { dir ->
new File(dir.asFile, templateName)
}

def destPathProvider = templateTaskProvider.flatMap { templateTask ->
project.providers.provider {
getDestPath(destPathTemplate, templateTask)
}
}

// Configure task with lazy providers
configureTaskLazily(task, renderedFileProvider, destPathProvider)
}

task.doFirst {
File postInstallCommand = new File(outputDir, POST_INSTALL_TEMPLATE)
task.postInstall(postInstallCommand.text)
}
// Add postInstall content from generated template
// Use providers.fileContents() which is configuration-cache-safe
def postInstallFileProvider = project.layout.buildDirectory.file(
"daemon/${cleanedName}/${task.name}/${POST_INSTALL_TEMPLATE}"
)

// Use fileContents provider which properly handles file reading for config cache
def postInstallContentProvider = project.providers.fileContents(postInstallFileProvider)
.asText
.orElse('')

// Add the file content to postInstallCommands
task.exten.postInstallCommands.addAll(
postInstallContentProvider.map { String content ->
content?.trim() ? [content] : []
}
)
}
}
}
Expand All @@ -142,14 +161,33 @@ class OspackageDaemonPlugin implements Plugin<Project> {
}
}

@CompileDynamic
private void configureTaskLazily(SystemPackagingTask task, def renderedFileProvider, def destPathProvider) {
task.from(renderedFileProvider) {
// Use closures for lazy evaluation during copy execution
into({
String destPath = destPathProvider.get()
int slashIdx = destPath.lastIndexOf('/')
destPath.substring(0, slashIdx)
})
rename({ String filename ->
String destPath = destPathProvider.get()
int slashIdx = destPath.lastIndexOf('/')
destPath.substring(slashIdx + 1)
})
FilePermissionUtil.setFilePermission(it, 0555)
user 'root'
}
}

@CompileDynamic
private void addDaemonToProject(Project project, Closure closure) {
project.ext.daemon = closure
}

private String getDestPath(String destPathTemplate, DaemonTemplateTask templateTask) {
GStringTemplateEngine engine = new GStringTemplateEngine()
def destPath = engine.createTemplate(destPathTemplate).make(templateTask.getContext()).toString()
def destPath = engine.createTemplate(destPathTemplate).make(templateTask.getContext().get()).toString()
destPath
}

Expand Down
Loading
Loading