elasticsearch/distribution/packages/build.gradle
2024-06-10 15:12:38 -07:00

558 lines
19 KiB
Groovy

/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import org.apache.tools.ant.filters.ReplaceTokens
import org.elasticsearch.gradle.LoggedExec
import org.elasticsearch.gradle.OS
import org.elasticsearch.gradle.internal.info.BuildParams
import org.redline_rpm.header.Flags
import java.nio.file.Files
import java.nio.file.Path
import java.util.regex.Matcher
import java.util.regex.Pattern
/*****************************************************************************
* Deb and rpm configuration *
*****************************************************************************
*
* The general strategy here is to build a directory on disk that contains
* stuff that needs to be copied into the distributions. This is
* important for two reasons:
* 1. ospackage wants to copy the directory permissions that it sees off of the
* filesystem. If you ask it to create a directory that doesn't already
* exist on disk it petulantly creates it with 0755 permissions, no matter
* how hard you try to convince it otherwise.
* 2. Convincing ospackage to pick up an empty directory as part of a set of
* directories on disk is reasonably easy. Convincing it to just create an
* empty directory requires more wits than I have.
* 3. ospackage really wants to suck up some of the debian control scripts
* directly from the filesystem. It doesn't want to process them through
* any copy-style action.
*
* The following commands are useful when it comes to check the user/group
* and files permissions set within the RPM and DEB packages:
*
* rpm -qlp --dump path/to/elasticsearch.rpm
* dpkg -c path/to/elasticsearch.deb
*/
plugins {
id "com.netflix.nebula.ospackage-base" version "11.9.1"
}
['deb', 'rpm'].each { type ->
String packagingFiles = "build/packaging/${type}"
String taskName = "process${type.capitalize()}Files"
tasks.register(taskName, Copy) {
into packagingFiles
with copySpec {
from 'src/common'
from "src/${type}"
filter("tokens": expansionsForDistribution(type, false), ReplaceTokens.class)
}
into('etc/elasticsearch') {
with configFiles(type, false)
}
filter("tokens": expansionsForDistribution(type, false), ReplaceTokens.class)
doLast {
// create empty dirs, we set the permissions when configuring the packages
mkdir "${packagingFiles}/var/log/elasticsearch"
mkdir "${packagingFiles}/var/lib/elasticsearch"
mkdir "${packagingFiles}/usr/share/elasticsearch/plugins"
// bare empty dir for /etc/elasticsearch and /etc/elasticsearch/jvm.options.d
mkdir "${packagingFiles}/elasticsearch"
mkdir "${packagingFiles}/elasticsearch/jvm.options.d"
}
}
}
// Common configuration that is package dependent. This can't go in ospackage
// since we have different templated files that need to be consumed, but the structure
// is the same
def commonPackageConfig(String type, String architecture) {
return {
onlyIf("not running on windows") {
OS.current().equals(OS.WINDOWS) == false
}
dependsOn "process${type.capitalize()}Files"
packageName "elasticsearch"
if (type == 'deb') {
if (architecture == 'x64') {
arch('amd64')
} else {
assert architecture == 'aarch64': architecture
arch('arm64')
}
} else {
assert type == 'rpm': type
if (architecture == 'x64') {
arch('X86_64')
} else {
assert architecture == 'aarch64': architecture
arch('aarch64')
}
}
// Follow elasticsearch's file naming convention
String prefix = "${architecture == 'aarch64' ? 'aarch64-' : ''}${type}"
destinationDirectory = file("${prefix}/build/distributions")
archiveFileName.value(project.provider({ "${packageName}-${project.version}-${archString}.${type}" }))
String packagingFiles = "build/packaging/${type}"
String scripts = "${packagingFiles}/scripts"
preInstall file("${scripts}/preinst")
postInstall file("${scripts}/postinst")
preUninstall file("${scripts}/prerm")
postUninstall file("${scripts}/postrm")
if (type == 'rpm') {
postTrans file("${scripts}/posttrans")
}
// top level "into" directive is not inherited from ospackage for some reason, so we must
// specify it again explicitly for copying common files
String platform = 'linux-' + ((architecture == 'x64') ? 'x86_64' : architecture)
into('/usr/share/elasticsearch') {
into('bin') {
with binFiles(type, false)
}
from(rootProject.projectDir) {
include 'README.asciidoc'
filePermissions {
unix 0644
}
}
into('lib') {
with libFiles('linux', architecture)
}
into('modules') {
with modulesFiles('linux', architecture)
}
into('jdk') {
with jdkFiles(project, 'linux', architecture)
}
// we need to specify every intermediate directory in these paths so the package managers know they are explicitly
// intended to manage them; otherwise they may be left behind on uninstallation. duplicate calls of the same
// directory are fine
eachFile { FileCopyDetails fcp ->
String[] segments = fcp.relativePath.segments
for (int i = segments.length - 2; i > 2; --i) {
if (type == 'rpm') {
directory('/' + segments[0..i].join('/'), 0755)
}
if (segments[-2] == 'bin' || segments[-1] == 'jspawnhelper') {
fcp.permissions {
unix(0755)
}
} else {
fcp.permissions {
unix(0644)
}
}
}
}
}
// license files
if (type == 'deb') {
into("/usr/share/doc/${packageName}") {
from "${packagingFiles}/copyright"
filePermissions {
unix(0644)
}
}
} else {
assert type == 'rpm'
into('/usr/share/elasticsearch') {
from(rootProject.file('licenses')) {
include 'ELASTIC-LICENSE-2.0.txt'
rename { 'LICENSE.txt' }
}
filePermissions {
unix(0644)
}
}
}
// ========= config files =========
configurationFile '/etc/elasticsearch/elasticsearch.yml'
configurationFile '/etc/elasticsearch/elasticsearch-plugins.example.yml'
configurationFile '/etc/elasticsearch/jvm.options'
configurationFile '/etc/elasticsearch/log4j2.properties'
configurationFile '/etc/elasticsearch/role_mapping.yml'
configurationFile '/etc/elasticsearch/roles.yml'
configurationFile '/etc/elasticsearch/users'
configurationFile '/etc/elasticsearch/users_roles'
from("${packagingFiles}") {
dirPermissions {
unix(02750)
}
into('/etc')
permissionGroup 'elasticsearch'
setgid true
includeEmptyDirs true
createDirectoryEntry true
include("elasticsearch") // empty dir, just to add directory entry
include("elasticsearch/jvm.options.d") // empty dir, just to add directory entry
}
from("${packagingFiles}/etc/elasticsearch") {
into('/etc/elasticsearch')
dirPermissions {
unix(02750)
}
setgid = true
filePermissions {
unix(0660)
}
permissionGroup 'elasticsearch'
includeEmptyDirs true
createDirectoryEntry true
fileType CONFIG | NOREPLACE
}
String envFile = expansionsForDistribution(type, false)['path.env']
configurationFile envFile
into(new File(envFile).getParent()) {
fileType CONFIG | NOREPLACE
permissionGroup 'elasticsearch'
filePermissions {
unix(0660)
}
from "${packagingFiles}/env/elasticsearch"
}
// ========= systemd =========
into('/usr/lib/tmpfiles.d') {
from "${packagingFiles}/systemd/elasticsearch.conf"
filePermissions {
unix(0644)
}
}
into('/usr/lib/systemd/system') {
fileType CONFIG | NOREPLACE
from "${packagingFiles}/systemd/elasticsearch.service"
filePermissions {
unix(0644)
}
}
into('/usr/lib/sysctl.d') {
fileType CONFIG | NOREPLACE
from "${packagingFiles}/systemd/sysctl/elasticsearch.conf"
filePermissions {
unix(0644)
}
}
into('/usr/share/elasticsearch/bin') {
from "${packagingFiles}/systemd/systemd-entrypoint"
filePermissions {
unix(0755)
}
}
// ========= empty dirs =========
// NOTE: these are created under packagingFiles as empty, but the permissions are set here
Closure copyEmptyDir = { path, u, g, mode ->
File file = new File(path)
into(file.parent) {
from "${packagingFiles}/${file.parent}"
include file.name
includeEmptyDirs true
createDirectoryEntry true
user u
permissionGroup g
dirPermissions {
unix(mode)
}
setgid (mode == 02750)
}
}
copyEmptyDir('/var/log/elasticsearch', 'elasticsearch', 'elasticsearch', 02750)
copyEmptyDir('/var/lib/elasticsearch', 'elasticsearch', 'elasticsearch', 02750)
copyEmptyDir('/usr/share/elasticsearch/plugins', 'root', 'root', 0755)
// the oss package conflicts with the default distribution and vice versa
conflicts('elasticsearch-oss')
into '/usr/share/elasticsearch'
with noticeFile(false)
}
}
// this is package independent configuration
ospackage {
maintainer 'Elasticsearch Team <info@elastic.co>'
summary 'Distributed RESTful search engine built for the cloud'
packageDescription '''
Reference documentation can be found at
https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
and the 'Elasticsearch: The Definitive Guide' book can be found at
https://www.elastic.co/guide/en/elasticsearch/guide/current/index.html
'''.stripIndent().trim()
url 'https://www.elastic.co/'
// signing setup
if (project.hasProperty('signing.password') && BuildParams.isSnapshotBuild() == false) {
signingKeyId = project.hasProperty('signing.keyId') ? project.property('signing.keyId') : 'D88E42B4'
signingKeyPassphrase = project.property('signing.password')
signingKeyRingFile = project.hasProperty('signing.secretKeyRingFile') ?
project.file(project.property('signing.secretKeyRingFile')) :
new File(new File(project.providers.systemProperty('user.home').orElse('.gnupg').get()), 'secring.gpg')
}
// version found on oldest supported distro, centos-6
requires('coreutils', '8.4', GREATER | EQUAL)
fileMode 0644
dirMode 0755
user 'root'
permissionGroup 'root'
into '/usr/share/elasticsearch'
}
Closure commonDebConfig(String architecture) {
return {
configure(commonPackageConfig('deb', architecture))
// jdeb does not provide a way to set the License control attribute, and
// ospackage silently ignores setting it. This is probably because `License`
// is not actually a field in the Debian control file. So instead, we set
// the license as "custom field".
customFields['License'] = 'Elastic-License'
archiveVersion = project.version.replace('-', '~')
packageGroup 'web'
// versions found on oldest supported distro, centos-6
requires('bash', '4.1', GREATER | EQUAL)
requires('lsb-base', '4', GREATER | EQUAL)
requires 'libc6'
requires 'adduser'
into('/usr/share/lintian/overrides') {
from('src/deb/lintian/elasticsearch')
filePermissions {
unix(0644)
}
}
}
}
tasks.register('buildAarch64Deb', Deb) {
configure(commonDebConfig('aarch64'))
}
tasks.register('buildDeb', Deb) {
configure(commonDebConfig('x64'))
}
Closure commonRpmConfig(String architecture) {
return {
configure(commonPackageConfig('rpm', architecture))
license 'Elastic License'
packageGroup 'Application/Internet'
requires '/bin/bash'
obsoletes packageName, '7.0.0', Flags.LESS
prefix '/usr'
packager 'Elasticsearch'
archiveVersion = project.version.replace('-', '_')
release = '1'
os 'LINUX'
distribution 'Elasticsearch'
vendor 'Elasticsearch'
// TODO ospackage doesn't support icon but we used to have one
// without this the rpm will have parent dirs of any files we copy in, eg /etc/elasticsearch
addParentDirs false
}
}
tasks.register('buildAarch64Rpm', Rpm) {
configure(commonRpmConfig('aarch64'))
}
tasks.register('buildRpm', Rpm) {
configure(commonRpmConfig('x64'))
}
Closure dpkgExists = { it -> new File('/bin/dpkg-deb').exists() || new File('/usr/bin/dpkg-deb').exists() || new File('/usr/local/bin/dpkg-deb').exists() }
Closure rpmExists = { it -> new File('/bin/rpm').exists() || new File('/usr/bin/rpm').exists() || new File('/usr/local/bin/rpm').exists() }
Closure debFilter = { f -> f.name.endsWith('.deb') }
// This configures the default artifact for the distribution specific
// subprojects. We have subprojects because Gradle project substitutions
// can only bind to the default configuration of a project
subprojects {
apply plugin: 'distribution'
String buildTask = "build${it.name.replaceAll(/-[a-z]/) { it.substring(1).toUpperCase() }.capitalize()}"
ext.buildDist = parent.tasks.named(buildTask)
artifacts {
'default' buildDist
}
if (dpkgExists() || rpmExists()) {
// sanity checks if packages can be extracted
final File extractionDir = new File(buildDir, 'extracted')
File packageExtractionDir
if (project.name.contains('deb')) {
packageExtractionDir = new File(extractionDir, 'deb-extracted')
} else {
assert project.name.contains('rpm')
packageExtractionDir = new File(extractionDir, 'rpm-extracted')
}
tasks.register('checkExtraction', LoggedExec) {
dependsOn buildDist
doFirst {
delete(extractionDir)
extractionDir.mkdirs()
}
}
tasks.named("check").configure { dependsOn "checkExtraction" }
if (project.name.contains('deb')) {
tasks.named("checkExtraction").configure {
onlyIf("dpkg exists", dpkgExists)
commandLine 'dpkg-deb', '-x', "${-> buildDist.get().outputs.files.filter(debFilter).singleFile}", packageExtractionDir
}
} else {
assert project.name.contains('rpm')
tasks.named("checkExtraction").configure {
onlyIf("rpm exists", rpmExists)
final File rpmDatabase = new File(extractionDir, 'rpm-database')
commandLine 'rpm',
'--badreloc',
'--ignorearch',
'--ignoreos',
'--nodeps',
'--noscripts',
'--notriggers',
'--dbpath',
rpmDatabase,
'--relocate',
"/=${packageExtractionDir}",
'-i',
"${-> buildDist.get().outputs.files.singleFile}"
}
}
tasks.register("checkLicense") {
dependsOn buildDist, "checkExtraction"
}
tasks.named("check").configure { dependsOn "checkLicense" }
if (project.name.contains('deb')) {
tasks.named("checkLicense").configure {
onlyIf("dpkg exists", dpkgExists)
doLast {
Path copyrightPath
String expectedLicense
String licenseFilename
copyrightPath = packageExtractionDir.toPath().resolve("usr/share/doc/elasticsearch/copyright")
expectedLicense = "Elastic-License"
licenseFilename = "ELASTIC-LICENSE-2.0.txt"
final List<String> header = Arrays.asList(
"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/",
"Copyright: Elasticsearch B.V. <info@elastic.co>",
"License: " + expectedLicense
)
final List<String> licenseLines = Files.readAllLines(rootDir.toPath().resolve("licenses/" + licenseFilename))
final List<String> expectedLines = header + licenseLines.collect { " " + it }
assertLinesInFile(copyrightPath, expectedLines)
}
}
} else {
assert project.name.contains('rpm')
tasks.named("checkLicense").configure {
onlyIf("rpm exists", rpmExists)
doLast {
String licenseFilename
licenseFilename = "ELASTIC-LICENSE-2.0.txt"
final List<String> licenseLines = Files.readAllLines(rootDir.toPath().resolve("licenses/" + licenseFilename))
final Path licensePath = packageExtractionDir.toPath().resolve("usr/share/elasticsearch/LICENSE.txt")
assertLinesInFile(licensePath, licenseLines)
}
}
}
tasks.register("checkNotice") {
dependsOn buildDist, "checkExtraction"
onlyIf("${project.name.contains('deb') ? 'dpkg' : 'rpm'} exists") {
(project.name.contains('deb') && dpkgExists.call(it)) || (project.name.contains('rpm') && rpmExists.call(it))
}
doLast {
final List<String> noticeLines = Arrays.asList("Elasticsearch", "Copyright 2009-2024 Elasticsearch")
final Path noticePath = packageExtractionDir.toPath().resolve("usr/share/elasticsearch/NOTICE.txt")
assertLinesInFile(noticePath, noticeLines)
}
}
def checkLicenseMetadataTaskProvider = tasks.register('checkLicenseMetadata', LoggedExec) {
dependsOn buildDist, "checkExtraction"
}
tasks.named("check").configure {
dependsOn "checkNotice"
dependsOn(checkLicenseMetadataTaskProvider)
}
if (project.name.contains('deb')) {
checkLicenseMetadataTaskProvider.configure { LoggedExec exec ->
onlyIf("dpkg exists", dpkgExists)
exec.commandLine 'dpkg-deb', '--info', "${-> buildDist.get().outputs.files.filter(debFilter).singleFile}"
exec.getCaptureOutput().set(true)
doLast {
String expectedLicense
expectedLicense = "Elastic-License"
final Pattern pattern = Pattern.compile("\\s*License: (.+)")
final String[] actualLines = getOutput().split("\n")
int count = 0
for (final String actualLine : actualLines) {
final Matcher matcher = pattern.matcher(actualLine)
if (matcher.matches()) {
count++
final String actualLicense = matcher.group(1)
if (expectedLicense != actualLicense) {
throw new GradleException("expected license [${expectedLicense} for package info but found [${actualLicense}]")
}
}
}
if (count == 0) {
throw new GradleException("expected license [${expectedLicense}] for package info but found none in:\n${info}")
}
if (count > 1) {
throw new GradleException("expected a single license for package info but found [${count}] in:\n${info}")
}
}
}
} else {
assert project.name.contains('rpm')
checkLicenseMetadataTaskProvider.configure { LoggedExec exec ->
onlyIf("rpm exists", rpmExists)
exec.commandLine 'rpm', '-qp', '--queryformat', '%{License}', "${-> buildDist.get().outputs.files.singleFile}"
exec.getCaptureOutput().set(true)
doLast {
String license = getOutput()
String expectedLicense
expectedLicense = "Elastic License"
if (license != expectedLicense) {
throw new GradleException("expected license [${expectedLicense}] for [${-> buildDist.get().outputs.files.singleFile}] but was [${license}]")
}
}
}
}
}
}