/* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch licenses this file to you 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 * * http://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 */ import org.elasticsearch.gradle.LoggedExec import org.elasticsearch.gradle.MavenFilteringHack import org.elasticsearch.gradle.OS import org.elasticsearch.gradle.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 * MavenFilteringHack or any other 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 "nebula.ospackage-base" version "8.3.0" } void addProcessFilesTask(String type, boolean oss, boolean jdk) { String packagingFiles = "build/packaging/${oss ? 'oss-' : ''}${jdk ? '' : 'no-jdk-'}${type}" String taskName = "process${oss ? 'Oss' : ''}${jdk ? '' : 'NoJdk'}${type.capitalize()}Files" tasks.register(taskName, Copy) { into packagingFiles with copySpec { from 'src/common' from "src/${type}" MavenFilteringHack.filter(it, expansionsForDistribution(type, oss, jdk)) } into('etc/elasticsearch') { with configFiles(type, oss, jdk) } MavenFilteringHack.filter(it, expansionsForDistribution(type, oss, jdk)) 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" } } } addProcessFilesTask('deb', true, true) addProcessFilesTask('deb', true, false) addProcessFilesTask('deb', false, true) addProcessFilesTask('deb', false, false) addProcessFilesTask('rpm', true, true) addProcessFilesTask('rpm', true, false) addProcessFilesTask('rpm', false, true) addProcessFilesTask('rpm', false, false) // 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 Closure commonPackageConfig(String type, boolean oss, boolean jdk, String architecture) { return { onlyIf { OS.current().equals(OS.WINDOWS) == false } dependsOn "process${oss ? 'Oss' : ''}${jdk ? '' : 'NoJdk'}${type.capitalize()}Files" packageName "elasticsearch${oss ? '-oss' : ''}" 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 jdkString = jdk ? "" : "no-jdk-" String prefix = "${architecture == 'aarch64' ? 'aarch64-' : ''}${oss ? 'oss-' : ''}${jdk ? '' : 'no-jdk-'}${type}" destinationDirectory = file("${prefix}/build/distributions") // SystemPackagingTask overrides default archive task convention mappings, but doesn't provide a setter so we have to override the convention mapping itself conventionMapping.archiveFile = { objects.fileProperty().fileValue(file("${destinationDirectory.get()}/${packageName}-${project.version}-${jdkString}${archString}.${type}")) } String packagingFiles = "build/packaging/${oss ? 'oss-' : ''}${jdk ? '' : 'no-jdk-'}${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 into('/usr/share/elasticsearch') { into('bin') { with binFiles(type, oss, jdk) } from(rootProject.projectDir) { include 'README.asciidoc' fileMode 0644 } into('lib') { with libFiles(oss) } into('modules') { with modulesFiles(oss, 'linux-' + ((architecture == 'x64') ? 'x86_64' : architecture)) } if (jdk) { 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.mode = 0755 } else { fcp.mode = 0644 } } } } // license files if (type == 'deb') { into("/usr/share/doc/${packageName}") { from "${packagingFiles}/copyright" fileMode 0644 } } else { assert type == 'rpm' into('/usr/share/elasticsearch') { from(rootProject.file('licenses')) { include 'APACHE-LICENSE-2.0.txt' rename { 'LICENSE.txt' } } fileMode 0644 } } // ========= config files ========= configurationFile '/etc/elasticsearch/elasticsearch.yml' configurationFile '/etc/elasticsearch/jvm.options' configurationFile '/etc/elasticsearch/log4j2.properties' if (oss == false) { configurationFile '/etc/elasticsearch/role_mapping.yml' configurationFile '/etc/elasticsearch/roles.yml' configurationFile '/etc/elasticsearch/users' configurationFile '/etc/elasticsearch/users_roles' } from("${packagingFiles}") { dirMode 02750 into('/etc') permissionGroup 'elasticsearch' 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') dirMode 02750 fileMode 0660 permissionGroup 'elasticsearch' includeEmptyDirs true createDirectoryEntry true fileType CONFIG | NOREPLACE } String envFile = expansionsForDistribution(type, oss, jdk)['path.env'] configurationFile envFile into(new File(envFile).getParent()) { fileType CONFIG | NOREPLACE permissionGroup 'elasticsearch' fileMode 0660 from "${packagingFiles}/env/elasticsearch" } // ========= systemd ========= into('/usr/lib/tmpfiles.d') { from "${packagingFiles}/systemd/elasticsearch.conf" fileMode 0644 } into('/usr/lib/systemd/system') { fileType CONFIG | NOREPLACE from "${packagingFiles}/systemd/elasticsearch.service" fileMode 0644 } into('/usr/lib/sysctl.d') { fileType CONFIG | NOREPLACE from "${packagingFiles}/systemd/sysctl/elasticsearch.conf" fileMode 0644 } into('/usr/share/elasticsearch/bin') { from "${packagingFiles}/systemd/systemd-entrypoint" fileMode 0755 } // ========= sysV init ========= configurationFile '/etc/init.d/elasticsearch' into('/etc/init.d') { fileMode 0750 fileType CONFIG | NOREPLACE from "${packagingFiles}/init.d/elasticsearch" } // ========= 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 dirMode mode } } 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 ? '' : '-oss')) into '/usr/share/elasticsearch' with noticeFile(oss, jdk) } } apply plugin: 'nebula.ospackage-base' // this is package indepdendent configuration ospackage { maintainer 'Elasticsearch Team ' 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(System.getProperty('user.home'), '.gnupg'), '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(boolean oss, boolean jdk, String architecture) { return { configure(commonPackageConfig('deb', oss, jdk, architecture)) // jdeb does not provide a way to set the License control attribute, and ospackage // silently ignores setting it. Instead, we set the license as "custom field" customFields['License'] = 'ASL-2.0' 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') if (oss) { rename('elasticsearch', 'elasticsearch-oss') } } } } tasks.register('buildAarch64Deb', Deb) { configure(commonDebConfig(false, true, 'aarch64')) } tasks.register('buildDeb', Deb) { configure(commonDebConfig(false, true, 'x64')) } tasks.register('buildAarch64OssDeb', Deb) { configure(commonDebConfig(true, true, 'aarch64')) } tasks.register('buildOssDeb', Deb) { configure(commonDebConfig(true, true, 'x64')) } tasks.register('buildNoJdkDeb', Deb) { configure(commonDebConfig(false, false, 'x64')) } tasks.register('buildOssNoJdkDeb', Deb) { configure(commonDebConfig(true, false, 'x64')) } Closure commonRpmConfig(boolean oss, boolean jdk, String architecture) { return { configure(commonPackageConfig('rpm', oss, jdk, architecture)) license 'ASL 2.0' 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(false, true, 'aarch64')) } tasks.register('buildRpm', Rpm) { configure(commonRpmConfig(false, true, 'x64')) } tasks.register('buildAarch64OssRpm', Rpm) { configure(commonRpmConfig(true, true, 'aarch64')) } tasks.register('buildOssRpm', Rpm) { configure(commonRpmConfig(true, true, 'x64')) } tasks.register('buildNoJdkRpm', Rpm) { configure(commonRpmConfig(false, false, 'x64')) } tasks.register('buildOssNoJdkRpm', Rpm) { configure(commonRpmConfig(true, false, '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 dpkgExists commandLine 'dpkg-deb', '-x', "${-> buildDist.get().outputs.files.filter(debFilter).singleFile}", packageExtractionDir } } else { assert project.name.contains('rpm') tasks.named("checkExtraction").configure { onlyIf 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" } check.dependsOn "checkLicense" if (project.name.contains('deb')) { tasks.named("checkLicense").configure { onlyIf dpkgExists doLast { Path copyrightPath String expectedLicense String licenseFilename copyrightPath = packageExtractionDir.toPath().resolve("usr/share/doc/elasticsearch-oss/copyright") expectedLicense = "ASL-2.0" licenseFilename = "APACHE-LICENSE-2.0.txt" final List header = Arrays.asList("Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/", "Copyright: Elasticsearch B.V. ", "License: " + expectedLicense) final List licenseLines = Files.readAllLines(rootDir.toPath().resolve("licenses/" + licenseFilename)) final List expectedLines = header + licenseLines.collect { " " + it } assertLinesInFile(copyrightPath, expectedLines) } } } else { assert project.name.contains('rpm') tasks.named("checkLicense").configure { onlyIf rpmExists doLast { String licenseFilename = "APACHE-LICENSE-2.0.txt" final List 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') && dpkgExists.call(it)) || (project.name.contains('rpm') && rpmExists.call(it)) } doLast { final List noticeLines = Arrays.asList("Elasticsearch", "Copyright 2009-2018 Elasticsearch") final Path noticePath = packageExtractionDir.toPath().resolve("usr/share/elasticsearch/NOTICE.txt") assertLinesInFile(noticePath, noticeLines) } } tasks.named("check").configure { dependsOn "checkNotice" } tasks.register('checkLicenseMetadata', LoggedExec) { dependsOn buildDist, "checkExtraction" } check.dependsOn checkLicenseMetadata if (project.name.contains('deb')) { checkLicenseMetadata { LoggedExec exec -> onlyIf dpkgExists final ByteArrayOutputStream output = new ByteArrayOutputStream() exec.commandLine 'dpkg-deb', '--info', "${-> buildDist.get().outputs.files.filter(debFilter).singleFile}" exec.standardOutput = output doLast { String expectedLicense = "ASL-2.0" final Pattern pattern = Pattern.compile("\\s*License: (.+)") final String info = output.toString('UTF-8') final String[] actualLines = info.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') checkLicenseMetadata { LoggedExec exec -> onlyIf rpmExists final ByteArrayOutputStream output = new ByteArrayOutputStream() exec.commandLine 'rpm', '-qp', '--queryformat', '%{License}', "${-> buildDist.get().outputs.files.singleFile}" exec.standardOutput = output doLast { String license = output.toString('UTF-8') String expectedLicense = "ASL-2.0" if (license != expectedLicense) { throw new GradleException("expected license [${expectedLicense}] for [${-> buildDist.get().outputs.files.singleFile}] but was [${license}]") } } } } } }