From 4926eb71e4e6c6b7ba53bedb4458403e05800ce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Wed, 5 Jan 2022 14:32:19 +0100 Subject: [PATCH] Use Gradle's VersionCatalog for dependencies in a single place (#2853) This PR enables the usage of Gradle 7 Version Catalog. The TOML file is used, in a way that _should_ be compatible with RenovateBot. The versions contain: - a set of `baseline` versions (replaces equivalents in gradle.properties) - direct versions which are used in several places The remaining versions are directly defined inline in the libraries and plugins sections. In addition, the compatibility of the various syntaxes used for versions/version references with Renovate Bot has been separately validated. Renovate now detects the Kotlin version used and we pin it to 1.5.x updates only. --- .github/renovate.json | 5 +++ benchmarks/build.gradle | 12 +++--- build.gradle | 63 +++++++------------------------ buildSrc/build.gradle | 4 +- buildSrc/settings.gradle | 26 +++++++++++++ gradle.properties | 6 +-- gradle/libs.versions.toml | 53 ++++++++++++++++++++++++++ reactor-core/build.gradle | 77 +++++++++++++++++++++----------------- reactor-test/build.gradle | 24 ++++++------ reactor-tools/build.gradle | 24 ++++++------ settings.gradle | 6 ++- 11 files changed, 179 insertions(+), 121 deletions(-) create mode 100644 buildSrc/settings.gradle create mode 100644 gradle/libs.versions.toml diff --git a/.github/renovate.json b/.github/renovate.json index db03f8ded7..ea5f5a0bfd 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -71,6 +71,11 @@ "groupName": "Micrometer 1.3.0", "groupSlug": "micrometer", "allowedVersions": "=1.3.0" + }, + { + "matchPackagePrefixes": ["org.jetbrains.kotlin"], + "groupName": "Kotlin", + "allowedVersions": "<1.6.0" } ] } diff --git a/benchmarks/build.gradle b/benchmarks/build.gradle index 105caad1e1..14b5c55cb3 100644 --- a/benchmarks/build.gradle +++ b/benchmarks/build.gradle @@ -12,17 +12,17 @@ configurations { dependencies { // Use the baseline to avoid using new APIs in the benchmarks - compileOnly "io.projectreactor:reactor-core:${perfBaselineVersion}" - compileOnly "com.google.code.findbugs:jsr305:${findbugsVersion}" + compileOnly libs.reactor.perfBaseline.core + compileOnly libs.jsr305 - implementation "org.openjdk.jmh:jmh-core:${jmhVersion}" - implementation "io.projectreactor.addons:reactor-extra:3.4.6", { + implementation libs.jmh.core + implementation libs.reactor.perfBaseline.extra, { exclude group: 'io.projectreactor', module: 'reactor-core' } - annotationProcessor "org.openjdk.jmh:jmh-generator-annprocess:${jmhVersion}" + annotationProcessor libs.jmh.annotations.processor current project(':reactor-core') - baseline "io.projectreactor:reactor-core:${perfBaselineVersion}", { + baseline libs.reactor.perfBaseline.core, { changing = true } } diff --git a/build.gradle b/build.gradle index e76f93af31..85905f77e0 100644 --- a/build.gradle +++ b/build.gradle @@ -18,32 +18,28 @@ import org.gradle.util.VersionNumber import java.text.SimpleDateFormat buildscript { - // we define kotlin version for benefit of both core and test (see kotlin-gradle-plugin below) - ext.kotlinVersion = '1.5.31' repositories { mavenCentral() maven { url "https://repo.spring.io/plugins-release" } } - dependencies { - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}" - classpath "org.jfrog.buildinfo:build-info-extractor-gradle:4.25.5" //applied in individual submodules - } } plugins { - id "com.github.johnrengelman.shadow" version "7.1.2" - id 'org.asciidoctor.jvm.convert' version '3.3.2' apply false - id 'org.asciidoctor.jvm.pdf' version '3.3.2' apply false - id "me.champeau.gradle.japicmp" version "0.3.0" - id "de.undercouch.download" version "4.1.2" - id "org.unbroken-dome.test-sets" version "4.0.0" apply false + alias(libs.plugins.kotlin) + alias(libs.plugins.artifactory) + alias(libs.plugins.shadow) + alias(libs.plugins.asciidoctor.convert) apply false + alias(libs.plugins.asciidoctor.pdf) apply false + alias(libs.plugins.japicmp) + alias(libs.plugins.download) + alias(libs.plugins.testsets) // note: build scan plugin now must be applied in settings.gradle // plugin portal is now outdated due to bintray sunset, at least for artifactory gradle plugin - id 'biz.aQute.bnd.builder' version '6.1.0' apply false - id 'io.spring.nohttp' version '0.0.10' - id "io.github.reyerizo.gradle.jcstress" version "0.8.13" apply false - id "com.diffplug.spotless" version "6.1.0" + alias(libs.plugins.bnd) apply false + alias(libs.plugins.nohttp) + alias(libs.plugins.jcstress) apply false + alias(libs.plugins.spotless) } apply plugin: "io.reactor.gradle.detect-ci" @@ -80,40 +76,9 @@ ext { } /* - * Note that some versions can be bumped by a script. - * These are found in `gradle.properties`... - * - * Versions not necessarily bumped by a script (testing, etc...) below: + * Note that all dependencies and their versions are now defined in + * ./gradle/libs.versions.toml */ - // Misc not often upgraded - jsr166BackportVersion = '1.0.0.RELEASE' - // Used as a way to get jsr305 annotations. - // 3.0.1 is the last version that has the 'annotations' jar needed on the compile classpath - findbugsVersion = '3.0.1' - - // Blockhound - blockhoundVersion = '1.0.6.RELEASE' - - // Logging - slf4jVersion = '1.7.32' - logbackVersion = '1.2.10' - - // Testing - jUnitPlatformVersion = '5.8.2' //needs to be manually synchronized in buildSrc - assertJVersion = '3.22.0' //needs to be manually synchronized in buildSrc - mockitoVersion = '4.2.0' - awaitilityVersion = '4.1.1' - throwingFunctionVersion = '1.5.1' - javaObjectLayoutVersion = '0.16' - testNgVersion = '7.4.0' - archUnitVersion = '0.22.0' - - // For reactor-tools - byteBuddyVersion = '1.12.6' //dependency, but plugin usage is now in a mock build done via TestKit - cgLibVersion = '3.3.0' - - // JMH - jmhVersion = '1.34' } // only publish scan if a specific gradle entreprise server is passed diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 3d2062700e..0548f67f2f 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -24,8 +24,8 @@ repositories { } dependencies { - testImplementation("org.assertj:assertj-core:3.22.0") - testImplementation platform("org.junit:junit-bom:5.8.2") + testImplementation libs.assertj + testImplementation platform(libs.junit.bom) testImplementation "org.junit.jupiter:junit-jupiter-api" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine" } diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle new file mode 100644 index 0000000000..889a868a8f --- /dev/null +++ b/buildSrc/settings.gradle @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2021 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed 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 + * + * https://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 specific language governing permissions and + * limitations under the License. + */ + +enableFeaturePreview("VERSION_CATALOGS") + +//import the catalog from main project +dependencyResolutionManagement { + versionCatalogs { + libs { + from(files("../gradle/libs.versions.toml")) + } + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index b873a20ef9..6bebe75c7f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,2 @@ -micrometerVersion=1.3.0 version=3.4.14-SNAPSHOT -reactiveStreamsVersion=1.0.3 -compatibleVersion=3.4.13 -bomVersion=2020.0.14 -perfBaselineVersion=3.4.13 +bomVersion=2020.0.14 \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000000..81abcbbd56 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,53 @@ +[versions] +# Baselines, should be updated on every release +baseline-core-api = "3.4.13" +baselinePerfCore = "3.4.13" +baselinePerfExtra = "3.3.8.RELEASE" + +# Other shared versions +asciidoctor = "3.3.2" +bytebuddy = "1.12.6" +jmh = "1.34" +junit = "5.8.2" +kotlin = "1.5.31" +reactiveStreams = "1.0.3" + +[libraries] +archUnit = "com.tngtech.archunit:archunit:0.22.0" +assertj = "org.assertj:assertj-core:3.22.0" +awaitility = "org.awaitility:awaitility:4.1.1" +blockhound = "io.projectreactor.tools:blockhound:1.0.6.RELEASE" +byteBuddy-agent = { module = "net.bytebuddy:byte-buddy-agent", version.ref = "bytebuddy" } +byteBuddy-api = { module = "net.bytebuddy:byte-buddy", version.ref = "bytebuddy" } +cglib = "cglib:cglib:3.3.0" +javaObjectLayout = "org.openjdk.jol:jol-core:0.16" +jmh-annotations-processor = { module = "org.openjdk.jmh:jmh-generator-annprocess", version.ref = "jmh" } +jmh-core = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" } +jsr166backport = "io.projectreactor:jsr166:1.0.0.RELEASE" +jsr305 = "com.google.code.findbugs:jsr305:3.0.1" +junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" } +kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } +logback = "ch.qos.logback:logback-classic:1.2.10" +micrometer = "io.micrometer:micrometer-core:1.3.0" +mockito = "org.mockito:mockito-core:4.2.0" +reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } +reactiveStreams-tck = { module = "org.reactivestreams:reactive-streams-tck", version.ref = "reactiveStreams" } +reactor-perfBaseline-core = { module = "io.projectreactor:reactor-core", version.ref = "baselinePerfCore" } +reactor-perfBaseline-extra = { module = "io.projectreactor.addons:reactor-extra", version.ref = "baselinePerfExtra" } +slf4j = "org.slf4j:slf4j-api:1.7.32" +testNg = "org.testng:testng:7.4.0" +throwingFunction = "com.pivovarit:throwing-function:1.5.1" + +[plugins] +artifactory = { id = "com.jfrog.artifactory", version = "4.25.5" } +asciidoctor-convert = { id = "org.asciidoctor.jvm.convert", version.ref = "asciidoctor" } +asciidoctor-pdf = { id = "org.asciidoctor.jvm.pdf", version.ref = "asciidoctor" } +bnd = { id = "biz.aQute.bnd.builder", version = "6.1.0" } +download = { id = "de.undercouch.download", version = "4.1.2" } +japicmp = { id = "me.champeau.gradle.japicmp", version = "0.3.0" } +jcstress = { id = "io.github.reyerizo.gradle.jcstress", version = "0.8.13" } +kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +nohttp = { id = "io.spring.nohttp", version = "0.0.10" } +shadow = { id = "com.github.johnrengelman.shadow", version = "7.1.2" } +spotless = { id = "com.diffplug.spotless", version = "6.1.0" } +testsets = { id = "org.unbroken-dome.test-sets", version = "4.0.0" } diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index ed32724a55..f0ad4f7caf 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -57,64 +57,73 @@ configurations { dependencies { // Reactive Streams - api "org.reactivestreams:reactive-streams:${reactiveStreamsVersion}" - tckTestImplementation ("org.reactivestreams:reactive-streams-tck:${reactiveStreamsVersion}") { - // without this exclusion, testng brings an old version of junit which *embeds* an old version of hamcrest - // which gets picked up first and that we don't want. TCK runs fine w/o (old) junit 4. + api libs.reactiveStreams + tckTestImplementation (libs.reactiveStreams.tck) { + /* + Without this exclusion, testng brings an old version of junit (3.8.1). + TestNG later versions can also bring JUnit 4.x which themselves bring an old version of hamcrest + which gets picked up first and that we don't want. TCK runs fine w/o (old) junit 3/4. + + We exclude the old JUnit, and for safety we also explicitly require latest versions of + TestNG separately below. + + reactive streams 1.0.3 -> testng 5.14.10 -> Junit 3.8.1 + testng 6.8.5 -> Junit 4.10 -> Hamcrest 1.1 + testng 7.4.0 (latest as of this comment) -> JUnit 4.13 -> Hamcrest 1.3 + */ exclude group: 'junit', module: 'junit' } + tckTestImplementation libs.testNg // JSR-305 annotations - compileOnly "com.google.code.findbugs:jsr305:$findbugsVersion" - testCompileOnly "com.google.code.findbugs:jsr305:$findbugsVersion" + compileOnly libs.jsr305 + testCompileOnly libs.jsr305 // Optional Logging Operator - compileOnly "org.slf4j:slf4j-api:$slf4jVersion" - testCompileOnly "org.slf4j:slf4j-api:$slf4jVersion" + compileOnly libs.slf4j + testCompileOnly libs.slf4j // Optional Metrics - compileOnly "io.micrometer:micrometer-core:$micrometerVersion" + compileOnly libs.micrometer // Not putting kotlin-stdlib as implementation to not force it as a transitive lib - compileOnly "org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}" - testImplementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}" + compileOnly libs.kotlin.stdlib + testImplementation libs.kotlin.stdlib // Optional BlockHound support - compileOnly "io.projectreactor.tools:blockhound:$blockhoundVersion" + compileOnly libs.blockhound // Also make BlockHound visible in the CP of dedicated testset - blockHoundTestImplementation "io.projectreactor.tools:blockhound:$blockhoundVersion" + blockHoundTestImplementation libs.blockhound // Optional JDK 9 Converter - jsr166backport "io.projectreactor:jsr166:$jsr166BackportVersion" + jsr166backport libs.jsr166backport - testImplementation platform("org.junit:junit-bom:${jUnitPlatformVersion}") + // Testing + testImplementation platform(libs.junit.bom) testImplementation "org.junit.jupiter:junit-jupiter-api" testImplementation "org.junit.platform:junit-platform-launcher" testImplementation "org.junit.jupiter:junit-jupiter-params" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine" - - testImplementation "ch.qos.logback:logback-classic:$logbackVersion" //need to access API to decrease some tests verbosity - // Testing testImplementation(project(":reactor-test")) { exclude module: 'reactor-core' } - - testImplementation "org.assertj:assertj-core:$assertJVersion" - testImplementation "org.mockito:mockito-core:$mockitoVersion" - testImplementation "org.openjdk.jol:jol-core:$javaObjectLayoutVersion" - testImplementation "org.awaitility:awaitility:$awaitilityVersion" - testImplementation "com.pivovarit:throwing-function:$throwingFunctionVersion" - testImplementation "com.tngtech.archunit:archunit:$archUnitVersion" + testImplementation libs.logback //need to access API to decrease some tests verbosity + testImplementation libs.assertj + testImplementation libs.mockito + testImplementation libs.javaObjectLayout + testImplementation libs.awaitility + testImplementation libs.throwingFunction + testImplementation libs.archUnit // withMicrometerTest is a test-set that validates what happens when micrometer *IS* // on the classpath. Needs sourceSets.test.output because tests there use helpers like AutoDisposingRule etc. - withMicrometerTestImplementation "io.micrometer:micrometer-core:$micrometerVersion" + withMicrometerTestImplementation libs.micrometer withMicrometerTestImplementation sourceSets.test.output - jcstressImplementation(project(":reactor-test")) { - exclude module: 'reactor-core' + jcstressImplementation(project(":reactor-test")) { + exclude module: 'reactor-core' } - jcstressImplementation "ch.qos.logback:logback-classic:$logbackVersion" + jcstressImplementation libs.logback } @@ -122,8 +131,8 @@ task downloadBaseline(type: Download) { onlyIfNewer true compress true - src "${repositories.mavenCentral().url}io/projectreactor/reactor-core/$compatibleVersion/reactor-core-${compatibleVersion}.jar" - dest "${buildDir}/baselineLibs/reactor-core-${compatibleVersion}.jar" + src "${repositories.mavenCentral().url}io/projectreactor/reactor-core/${libs.versions.baseline.core.api.get()}/reactor-core-${libs.versions.baseline.core.api.get()}.jar" + dest "${buildDir}/baselineLibs/reactor-core-${libs.versions.baseline.core.api.get()}.jar" } task japicmp(type: JapicmpTask) { @@ -131,16 +140,16 @@ task japicmp(type: JapicmpTask) { println "Offline: skipping downloading of baseline and JAPICMP" enabled = false } - else if ("$compatibleVersion" == "SKIP") { + else if ("${libs.versions.baseline.core.api.get()}" == "SKIP") { println "SKIP: Instructed to skip the baseline comparison" enabled = false } else { - println "Will download and perform baseline comparison with ${compatibleVersion}" + println "Will download and perform baseline comparison with ${libs.versions.baseline.core.api.get()}" dependsOn(downloadBaseline) } - oldClasspath = files("${buildDir}/baselineLibs/reactor-core-${compatibleVersion}.jar") + oldClasspath = files("${buildDir}/baselineLibs/reactor-core-${libs.versions.baseline.core.api.get()}.jar") newClasspath = files(jar.archiveFile) onlyBinaryIncompatibleModified = false failOnModification = false diff --git a/reactor-test/build.gradle b/reactor-test/build.gradle index 334a05c278..3c8e06d94a 100644 --- a/reactor-test/build.gradle +++ b/reactor-test/build.gradle @@ -38,27 +38,27 @@ ext { dependencies { api project(":reactor-core") - compileOnly "com.google.code.findbugs:jsr305:${findbugsVersion}" + compileOnly libs.jsr305 // Not putting kotlin-stdlib as implementation to not force it as a transitive lib - compileOnly "org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}" - testImplementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}" + compileOnly libs.kotlin.stdlib + testImplementation libs.kotlin.stdlib - testImplementation platform("org.junit:junit-bom:${jUnitPlatformVersion}") + testImplementation platform(libs.junit.bom) testImplementation "org.junit.jupiter:junit-jupiter-api" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine" - testRuntimeOnly "ch.qos.logback:logback-classic:$logbackVersion" + testRuntimeOnly libs.logback - testImplementation "org.assertj:assertj-core:$assertJVersion" - testImplementation "org.mockito:mockito-core:$mockitoVersion" + testImplementation libs.assertj + testImplementation libs.mockito } task downloadBaseline(type: Download) { onlyIfNewer true compress true - src "${repositories.mavenCentral().url}io/projectreactor/reactor-test/$compatibleVersion/reactor-test-${compatibleVersion}.jar" - dest "${buildDir}/baselineLibs/reactor-test-${compatibleVersion}.jar" + src "${repositories.mavenCentral().url}io/projectreactor/reactor-test/${libs.versions.baseline.core.api.get()}/reactor-test-${libs.versions.baseline.core.api.get()}.jar" + dest "${buildDir}/baselineLibs/reactor-test-${libs.versions.baseline.core.api.get()}.jar" finalizedBy { japicmp } } @@ -68,16 +68,16 @@ task japicmp(type: JapicmpTask) { println "Offline: skipping downloading of baseline and JAPICMP" enabled = false } - else if ("$compatibleVersion" == "SKIP") { + else if ("${libs.versions.baseline.core.api.get()}" == "SKIP") { println "SKIP: Instructed to skip the baseline comparison" enabled = false } else { - println "Will download and perform baseline comparison with ${compatibleVersion}" + println "Will download and perform baseline comparison with ${libs.versions.baseline.core.api.get()}" dependsOn(downloadBaseline) } - oldClasspath = files("${buildDir}/baselineLibs/reactor-test-${compatibleVersion}.jar") + oldClasspath = files("${buildDir}/baselineLibs/reactor-test-${libs.versions.baseline.core.api.get()}.jar") newClasspath = files(jar.archiveFile) onlyBinaryIncompatibleModified = true failOnModification = true diff --git a/reactor-tools/build.gradle b/reactor-tools/build.gradle index b8cd92e69d..bf762ef615 100644 --- a/reactor-tools/build.gradle +++ b/reactor-tools/build.gradle @@ -35,28 +35,28 @@ configurations { dependencies { api project(":reactor-core") - compileOnly "com.google.code.findbugs:jsr305:${findbugsVersion}" - compileOnly "com.google.code.findbugs:annotations:${findbugsVersion}" + compileOnly libs.jsr305 + compileOnly libs.jsr305 - shaded "net.bytebuddy:byte-buddy-agent:$byteBuddyVersion" - shaded "net.bytebuddy:byte-buddy:$byteBuddyVersion" + shaded libs.byteBuddy.api + shaded libs.byteBuddy.agent for (dependency in project.configurations.shaded.dependencies) { compileOnly(dependency) testRuntimeOnly(dependency) javaAgentTestRuntimeOnly(dependency) } - testImplementation platform("org.junit:junit-bom:${jUnitPlatformVersion}") + testImplementation platform(libs.junit.bom) testImplementation "org.junit.jupiter:junit-jupiter-api" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine" - testImplementation "org.assertj:assertj-core:$assertJVersion" - testImplementation "cglib:cglib:$cgLibVersion" + testImplementation libs.assertj + testImplementation libs.cglib - jarFileTestImplementation "org.assertj:assertj-core:$assertJVersion" + jarFileTestImplementation libs.assertj buildPluginTestImplementation gradleTestKit() - buildPluginTestImplementation platform("org.junit:junit-bom:${jUnitPlatformVersion}") + buildPluginTestImplementation platform(libs.junit.bom) buildPluginTestImplementation "org.junit.jupiter:junit-jupiter-api" buildPluginTestRuntimeOnly "org.junit.jupiter:junit-jupiter-engine" } @@ -176,9 +176,9 @@ task generateMockGradle(type: Copy) { filter(ReplaceTokens, tokens: [ CORE: coreJar, AGENT: agentJar, - REACTIVE_STREAMS_VERSION: rootProject.ext.reactiveStreamsVersion, - JUNIT_BOM_VERSION: rootProject.ext.jUnitPlatformVersion, - BYTE_BUDDY_VERSION: rootProject.ext.byteBuddyVersion + REACTIVE_STREAMS_VERSION: libs.versions.reactiveStreams.get(), + JUNIT_BOM_VERSION: libs.versions.junit.get(), + BYTE_BUDDY_VERSION: libs.versions.bytebuddy.get() ]) } diff --git a/settings.gradle b/settings.gradle index 2fbf2caa43..ca6be957a8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,4 +19,8 @@ plugins { rootProject.name = 'reactor' -include 'benchmarks', 'reactor-core', 'reactor-test', 'reactor-tools' \ No newline at end of file +include 'benchmarks', 'reactor-core', 'reactor-test', 'reactor-tools' + +//libs catalog is declared in ./gradle/libs.versions.toml +//TODO remove once Version Catalogs are stabilized. It is also activated in buildSrc +enableFeaturePreview("VERSION_CATALOGS") \ No newline at end of file