├── .editorconfig ├── .github ├── CODEOWNERS ├── codecov.yml ├── dependabot.yml ├── labeler.yml └── workflows │ ├── build.yml │ ├── dependabot.yml │ ├── labeler.yml │ ├── release-auto.yml │ ├── release.yml │ └── upgrade-gradle-wrapper.yml ├── .gitignore ├── DEVELOPMENT.md ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── scripts └── git │ └── pre-commit ├── settings.gradle.kts └── src ├── integration └── kotlin │ └── com │ └── coditory │ └── gradle │ └── integration │ ├── CommandLineAcceptanceTest.kt │ ├── GenerateClasspathTest.kt │ ├── GenerateWithCustomAttributesTest.kt │ └── MultiModuleAcceptanceTest.kt ├── main └── kotlin │ └── com │ └── coditory │ └── gradle │ └── manifest │ ├── ManifestParameterizedPlugin.kt │ ├── ManifestPlugin.kt │ ├── ManifestPluginExtension.kt │ ├── ManifestTask.kt │ └── attributes │ ├── HostNameResolver.kt │ ├── ManifestAttributeResolver.kt │ └── RepositoryValueSource.kt └── test └── kotlin └── com └── coditory └── gradle └── manifest ├── GenerateManifestTest.kt ├── GenerateManifestWithoutDisabledAttributesTest.kt ├── PluginSetupTest.kt ├── SetupManifestAttributesTest.kt └── base ├── GradleTestVersions.kt ├── ManifestExtractor.kt ├── ManifestPluginWithStubs.kt ├── SystemOutputCapturer.kt ├── SystemProperties.kt ├── TestProject.kt ├── TestProjectBuilder.kt ├── TestRepository.kt ├── UpdatableFixedClock.kt └── Versions.kt /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.{kt,kts}] 12 | indent_size = 4 13 | ktlint_code_style = intellij_idea 14 | ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = unset 15 | ktlint_ignore_back_ticked_identifier = true 16 | ktlint_function_signature_body_expression_wrapping = default 17 | ktlint_standard_function-expression-body = disabled 18 | ktlint_standard_class-signature = disabled -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @coditory/reviewers 2 | 3 | # Exclusions so changes made by dependabot and other bots could be auto reviewed by Coditory App. 4 | # Currently App cannot be a part of a team or CODEOWNERS file. 5 | # https://github.com/orgs/community/discussions/23064 6 | .github/workflows/* 7 | gradle/** 8 | **/build.gradle.kts 9 | **/settings.gradle.kts 10 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: off -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | groups: 8 | # merged and released instantly 9 | sec-updates: 10 | applies-to: security-updates 11 | patterns: 12 | - "*" 13 | # merged automatically 14 | dev-dependencies: 15 | patterns: 16 | - "*" 17 | 18 | - package-ecosystem: "gradle" 19 | directory: "/" 20 | schedule: 21 | interval: "daily" 22 | groups: 23 | # merged and released instantly 24 | sec-updates: 25 | applies-to: security-updates 26 | patterns: 27 | - "*" 28 | # merged automatically 29 | dev-dependencies: 30 | patterns: 31 | # gradle plugins 32 | - "*kotlin-gradle-plugin" 33 | - "com.coditory.integration-test" 34 | - "com.gradle.plugin-publish" 35 | - "org.jlleitschuh.gradle.ktlint" 36 | - "org.jetbrains.kotlinx.kover" 37 | # test dependencies 38 | - "org.assertj*" 39 | - "org.junit*" 40 | # merged and released automatically 41 | prod-dependencies: 42 | update-types: 43 | - "patch" 44 | - "minor" 45 | # requires human approval and has higher chance to fail build 46 | prod-dependencies-major: 47 | update-types: 48 | - "major" 49 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | backport: 2 | - head-branch: ['^v[0-9]+\.x\.x'] 3 | 4 | code: 5 | - changed-files: 6 | - any-glob-to-any-file: 7 | - "src/**" 8 | 9 | build: 10 | - changed-files: 11 | - any-glob-to-any-file: 12 | - "**/*.gradle*" 13 | 14 | ci: 15 | - changed-files: 16 | - any-glob-to-any-file: 17 | - ".github/**" 18 | 19 | documentation: 20 | - changed-files: 21 | - any-glob-to-any-file: 22 | - "**/*.md" 23 | - "docs/**" 24 | 25 | license: 26 | - changed-files: 27 | - any-glob-to-any-file: 28 | - "LICENSE" 29 | 30 | gradle: 31 | - changed-files: 32 | - any-glob-to-any-file: 33 | - "gradlew*" 34 | - ".gradle/**" 35 | - "gradle/**" 36 | - "setting.gradle*" 37 | 38 | gitignore: 39 | - changed-files: 40 | - any-glob-to-any-file: 41 | - ".gitignore" 42 | 43 | codestyle: 44 | - changed-files: 45 | - any-glob-to-any-file: 46 | - ".editorconfig" 47 | - ".idea/codeStyles/**" 48 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | push: 7 | branches-ignore: 8 | - 'gh-pages' 9 | 10 | jobs: 11 | build: 12 | uses: coditory/jvm-workflows/.github/workflows/build.yml@v1 13 | # Required to pass codecov token 14 | secrets: inherit 15 | with: 16 | java-version: 17 17 | build-command: ./gradlew build coverage 18 | -------------------------------------------------------------------------------- /.github/workflows/dependabot.yml: -------------------------------------------------------------------------------- 1 | name: "Dependabot" 2 | 3 | on: pull_request_target 4 | 5 | jobs: 6 | dependabot: 7 | uses: coditory/workflows/.github/workflows/dependabot.yml@v1 8 | secrets: inherit 9 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | name: "Labeler" 2 | 3 | on: pull_request_target 4 | 5 | jobs: 6 | labeler: 7 | uses: coditory/workflows/.github/workflows/labeler.yml@v1 8 | secrets: inherit 9 | -------------------------------------------------------------------------------- /.github/workflows/release-auto.yml: -------------------------------------------------------------------------------- 1 | name: Release Auto 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | security-updates-only: 7 | description: "Security updates only" 8 | type: boolean 9 | required: false 10 | default: false 11 | consider-snapshot: 12 | description: "Consider snapshot" 13 | type: boolean 14 | required: false 15 | default: false 16 | workflow_run: 17 | workflows: ["Build"] 18 | types: [completed] 19 | branches: 20 | - main 21 | - v*x.x 22 | schedule: 23 | # at 5:30 UTC every other month 24 | - cron: "30 5 1 */2 *" 25 | 26 | jobs: 27 | check: 28 | uses: coditory/workflows/.github/workflows/release-check.yml@v1 29 | secrets: inherit 30 | if: | 31 | github.event_name != 'workflow_run' 32 | || !contains(github.event.workflow_run.head_commit.message, '[ci-skip-build]') 33 | with: 34 | security-updates-only: ${{ inputs.security-updates-only || github.event_name == 'workflow_run' }} 35 | 36 | release: 37 | uses: ./.github/workflows/release.yml 38 | secrets: inherit 39 | needs: check 40 | if: needs.check.outputs.release == 'true' 41 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | branch: 7 | type: string 8 | description: Branch name to release 9 | required: true 10 | default: main 11 | section: 12 | type: choice 13 | description: Version section to increment 14 | options: 15 | - patch 16 | - minor 17 | - major 18 | required: true 19 | default: patch 20 | version: 21 | type: string 22 | description: ...or manually define version like 1.2.3, 1.2.3-suffix 23 | required: false 24 | # Called from release-auto 25 | workflow_call: 26 | inputs: 27 | branch: 28 | type: string 29 | required: false 30 | default: main 31 | section: 32 | type: string 33 | required: false 34 | default: patch 35 | version: 36 | type: string 37 | required: false 38 | 39 | jobs: 40 | release: 41 | uses: coditory/workflows/.github/workflows/release.yml@v1 42 | secrets: inherit 43 | with: 44 | branch: ${{ inputs.branch }} 45 | section: ${{ inputs.section }} 46 | version: ${{ inputs.version }} 47 | java-version: 17 48 | release-command: | 49 | ./gradlew publishPlugins \ 50 | -Pgradle.publish.key=$GRADLE_PUBLISH_KEY \ 51 | -Pgradle.publish.secret=$GRADLE_PUBLISH_SECRET \ 52 | -Pversion=$NEXT_VERSION 53 | version-command: ./gradlew version --quiet --no-scan 54 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-gradle-wrapper.yml: -------------------------------------------------------------------------------- 1 | name: Upgrade Gradle Wrapper 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | # at 5:30 UTC every month 7 | - cron: "30 5 1 * *" 8 | 9 | jobs: 10 | upgrade-gradle-wrapper: 11 | uses: coditory/jvm-workflows/.github/workflows/upgrade-gradle-wrapper.yml@v1 12 | secrets: inherit 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle 2 | .gradle 3 | .kotlin 4 | build 5 | !gradle/wrapper/gradle-wrapper.jar 6 | 7 | # IntelliJ 8 | out 9 | *.iml 10 | .idea/* 11 | !.idea/codeStyles 12 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | This is a development focused supplement to [CONTRIBUTING.md](https://github.com/coditory/.github/blob/main/CONTRIBUTING.md). 4 | 5 | ## Pre commit hook (optional) 6 | 7 | Installing pre-commit hook is optional but can save you some headache when pushing unformatted code. 8 | 9 | Installing git pre-commit hook that formats code with [Ktlint](https://pinterest.github.io/ktlint): 10 | 11 | ```sh 12 | cp scripts/git/pre-commit .git/hooks/pre-commit 13 | ``` 14 | 15 | ## Commit messages 16 | 17 | Before writing a commit message read [this article](https://chris.beams.io/posts/git-commit/). 18 | 19 | ## Build 20 | 21 | Before pushing any changes make sure project builds without errors with: 22 | 23 | ``` 24 | ./gradlew build 25 | ``` 26 | 27 | ## Code conventions 28 | 29 | This repository follows the [Kotlin coding conventions](https://kotlinlang.org/docs/reference/coding-conventions.html). 30 | That are enforced by ktlint and [.editorconfig](../.editorconfig). 31 | 32 | You can check style with: 33 | 34 | ``` 35 | ./gradlew ktlintCheck 36 | ``` 37 | 38 | ## Unit tests and coverage 39 | 40 | Use [JUnit 5](https://junit.org/junit5/docs/current/user-guide/) for testing. 41 | 42 | Uou can check coverage in `build/reports/kover/` after running: 43 | 44 | ``` 45 | ./gradlew test coverage 46 | ``` 47 | 48 | ## Validate changes locally 49 | 50 | Before submitting a pull request test your changes on a local project. 51 | There are few ways for testing locally a gradle plugin: 52 | 53 | **Publish plugin to the local maven repository** 54 | 55 | - Publish plugin to your local maven repository (`$HOME/.m2`) with: 56 | ```sh 57 | ./gradlew publishToMavenLocal -Pversion="" && ls -la ~/.m2/repository/com/coditory/gradle/manifest-plugin 58 | ``` 59 | - Add section to `settings.gradle.kts`: 60 | ```kt 61 | // Instruct a sample project to use maven local to find the plugin 62 | pluginManagement { 63 | repositories { 64 | mavenLocal() 65 | gradlePluginPortal() 66 | } 67 | } 68 | ``` 69 | - Add dependency: 70 | ```kt 71 | plugins { 72 | id("com.coditory.manifest") version "" 73 | } 74 | ``` 75 | 76 | **Import plugin jar** 77 | Add plugin jar to the sample project (that uses the tested plugin): 78 | 79 | ```kt 80 | buildscript { 81 | dependencies { 82 | classpath(files("/build/libs/manifest-plugin.jar")) 83 | } 84 | } 85 | 86 | apply(plugin = "com.coditory.manifest") 87 | ``` 88 | 89 | ## Validating plugin module metadata 90 | 91 | The easiest way to validate plugin's module metadata is to publish the plugin to a dummy local repository. 92 | 93 | Add to `build.gradle.kts`: 94 | 95 | ``` 96 | publishing { 97 | repositories { 98 | maven { 99 | name = "localPluginRepository" 100 | url = uri("./local-plugin-repository") 101 | } 102 | } 103 | } 104 | ``` 105 | 106 | ...and publish the plugin with: 107 | 108 | ``` 109 | ./gradlew publish -Pversion=0.0.1 110 | ``` 111 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Paweł Mendelski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Manifest Gradle Plugin 2 | 3 | [![Build](https://github.com/coditory/gradle-manifest-plugin/actions/workflows/build.yml/badge.svg)](https://github.com/coditory/gradle-manifest-plugin/actions/workflows/build.yml) 4 | [![Coverage](https://codecov.io/gh/coditory/gradle-manifest-plugin/branch/main/graph/badge.svg)](https://codecov.io/gh/coditory/gradle-manifest-plugin) 5 | [![Gradle Plugin Portal](https://img.shields.io/gradle-plugin-portal/v/com.coditory.manifest)](https://plugins.gradle.org/plugin/com.coditory.manifest) 6 | 7 | 8 | **Single responsibility** gradle plugin for generating project metadata 9 | to [jar manifest file](https://docs.oracle.com/javase/tutorial/deployment/jar/manifestindex.html). 10 | 11 | - Runs `manifest` task 12 | after [`processResources`](https://docs.gradle.org/current/userguide/java_plugin.html#sec:java_tasks) 13 | - `manifest` task creates manifest file in `build/resources/main/META-INF/MANIFEST.MF` 14 | 15 | ## Installing the plugin 16 | 17 | Add to your `build.gradle.kts`: 18 | 19 | ```gradle 20 | plugins { 21 | id("com.coditory.manifest") version "1.1.0" 22 | } 23 | ``` 24 | 25 | ## Generated manifest 26 | 27 | Sample `MANIFEST.MF` generated for default configuration: 28 | 29 | ``` 30 | Manifest-Version: 1.0 31 | Main-Class: com.coditory.Application 32 | Implementation-Title: sample-project 33 | Implementation-Group: com.coditory 34 | Implementation-Version: 0.0.1-SNAPSHOT 35 | Built-By: john.doe 36 | Built-Host: john-pc 37 | Built-Date: 2020-03-25T20:46:59Z 38 | Built-OS: Linux 4.15.0-91-generic amd64 39 | Built-JDK: 12.0.2 AdoptOpenJDK 40 | SCM-Repository: git@github.com:coditory/gradle-manifest-plugin.git 41 | SCM-Branch: refs/heads/master 42 | SCM-Commit-Message: Very important commit 43 | SCM-Commit-Hash: ef2c3dcabf1b0a87a90e098d1a6f0341f0ae1adf 44 | SCM-Commit-Author: John Doe 45 | SCM-Commit-Date: 2020-03-24T19:46:03Z 46 | ``` 47 | 48 | ## Usage 49 | 50 | This plugin automatically adds metadata to manifest file after processResources. There is nothing you need to do. 51 | 52 | For debug purposes you can run `manifest` task with some flags: 53 | 54 | ```sh 55 | # Generates manifest file to build/resources/main/META-INF/MANIFEST.MF 56 | ./gradlew manifest 57 | 58 | # Generates manifest and prints its content 59 | ./gradlew manifest --print 60 | 61 | # Generates manifest to src/main/resources/META-INF/MANIFEST.MF 62 | ./gradlew manifest --main 63 | ``` 64 | 65 | ## Overriding generated attributes 66 | 67 | To disable or override generated manifest attributes configure the plugin in `build.gradle` 68 | 69 | ```gradle 70 | manifest { 71 | buildAttributes = false 72 | implementationAttributes = true 73 | scmAttributes = false 74 | attributes = [ 75 | "Custom-1": "Custom-Value", 76 | "Custom-2": 123 77 | ] 78 | } 79 | ``` 80 | 81 | ## Generating classpath attribute 82 | 83 | To generate class path attribute with all compile dependencies prefixed with a custom directory use `classpathPrefix`: 84 | 85 | ```gradle 86 | manifest { 87 | classpathPrefix = "my-jars" 88 | } 89 | ``` 90 | 91 | See Java tutorial 92 | on [Adding Classes and Jars to Jar File's Classpath](https://docs.oracle.com/javase/tutorial/deployment/jar/downman.html) 93 | 94 | ## Reading `MANIFEST.MF` from project 95 | 96 | Take a look at a [gradle-manifest-plugin-sample](https://github.com/coditory/gradle-manifest-plugin-sample). 97 | You can copy [ManifestReader.java](https://github.com/coditory/gradle-manifest-plugin-sample/blob/master/src/main/java/com/coditory/sandbox/ManifestReader.java) (with [tests](https://github.com/coditory/gradle-manifest-plugin-sample/blob/master/src/test/groovy/com/coditory/sandbox/ManifestReaderTest.groovy)) to you own project. 98 | 99 | Reading `MANIFEST.MF` is tricky. There can be multiple `MANIFEST.MF` files on the classpath that comes from libraries. 100 | To read the correct `MANIFEST.MF` you need to filter them. Most often `Implementation-Title` is used to find the correct one. 101 | 102 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") version "2.1.21" 3 | id("java-gradle-plugin") 4 | id("maven-publish") 5 | id("com.gradle.plugin-publish") version "1.3.1" 6 | id("org.jlleitschuh.gradle.ktlint") version "12.1.2" 7 | id("org.jetbrains.kotlinx.kover") version "0.9.1" 8 | id("com.coditory.integration-test") version "2.2.4" 9 | } 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | dependencies { 16 | compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.10") 17 | implementation("org.eclipse.jgit:org.eclipse.jgit:7.3.0.202506031305-r") 18 | 19 | testImplementation("org.assertj:assertj-core:3.27.3") 20 | testImplementation("org.junit.jupiter:junit-jupiter-api:5.11.4") 21 | testImplementation("org.junit.jupiter:junit-jupiter-params:5.11.4") 22 | testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.11.4") 23 | } 24 | 25 | group = "com.coditory.gradle" 26 | 27 | kotlin { 28 | compilerOptions { 29 | allWarningsAsErrors = true 30 | } 31 | } 32 | 33 | java { 34 | toolchain { 35 | languageVersion = JavaLanguageVersion.of(17) 36 | } 37 | } 38 | 39 | ktlint { 40 | version = "1.4.0" 41 | } 42 | 43 | tasks.withType().configureEach { 44 | useJUnitPlatform() 45 | testLogging { 46 | events("passed", "failed", "skipped") 47 | setExceptionFormat("full") 48 | } 49 | } 50 | 51 | tasks.register("coverage") { 52 | dependsOn("koverXmlReport", "koverHtmlReport", "koverLog") 53 | } 54 | 55 | // Marking new version (incrementPatch [default], incrementMinor, incrementMajor) 56 | // ./gradlew markNextVersion -Prelease.incrementer=incrementMinor 57 | // Releasing the plugin: 58 | // ./gradlew release && ./gradlew publishPlugins 59 | gradlePlugin { 60 | website = "https://github.com/coditory/gradle-manifest-plugin" 61 | vcsUrl = "https://github.com/coditory/gradle-manifest-plugin" 62 | plugins { 63 | create("manifestPlugin") { 64 | id = "com.coditory.manifest" 65 | implementationClass = "com.coditory.gradle.manifest.ManifestPlugin" 66 | displayName = "Manifest plugin" 67 | description = "Writes project metadata to META-INF/MANIFEST.MF" 68 | tags = listOf("manifest", "project manifest plugin") 69 | } 70 | } 71 | } 72 | 73 | // Prints project version. 74 | // Usage: ./gradlew version --quiet 75 | tasks.register("version") { 76 | val version = project.version 77 | doLast { 78 | println(version) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | version=1.1.0 2 | org.gradle.configuration-cache=true 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coditory/gradle-manifest-plugin/d149e73a908827df483acba5fb2feea84e92ae6d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /scripts/git/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euf -o pipefail 3 | 4 | function run_ktlint { 5 | if ! command -v ktlint &>/dev/null; then 6 | echo -e "${RED}Please install Ktlint${ENDCOLOR} (https://pinterest.github.io/ktlint/latest/install/cli/#download-and-verification)" 7 | exit 1 8 | fi 9 | # https://pinterest.github.io/ktlint/0.49.0/install/cli/#git-hooks 10 | KT_FILES=$(git diff --name-only --cached --relative --diff-filter=ACMR -- '*.kt' '*.kts') 11 | if [ -n "$KT_FILES" ]; then 12 | echo -e "${BLUE}Ktlint: linting $(echo "$KT_FILES" | wc -l) files${ENDCOLOR}" 13 | start="$(date +%s)" 14 | echo -n "$KT_FILES" | tr '\n' ',' | ktlint --relative --format --patterns-from-stdin=',' 15 | echo -e "${GREEN}Ktlint: finished in $(($(date +%s) - start))s${ENDCOLOR}" 16 | echo "$KT_FILES" | xargs git add 17 | fi 18 | } 19 | 20 | if [ "${NO_COLOR:-}" != "false" ]; then 21 | RED="\e[31m" 22 | GREEN="\e[32m" 23 | BLUE="\e[34m" 24 | ENDCOLOR="\e[0m" 25 | else 26 | RED="" 27 | GREEN="" 28 | BLUE="" 29 | ENDCOLOR="" 30 | fi 31 | 32 | prestart="$(date +%s)" 33 | echo -e "${BLUE}Pre-commit: Starting${ENDCOLOR}" 34 | 35 | if [ ./scripts/git/pre-commit -nt .git/hooks/pre-commit ]; then 36 | cp -f ./scripts/git/pre-commit .git/hooks/pre-commit 37 | echo -e "${RED}Updated git pre-commit hook. Please re-run commit.${ENDCOLOR}" 38 | exit 1 39 | fi 40 | 41 | run_ktlint 42 | 43 | echo -e "${GREEN}Pre-commit: finished in $(($(date +%s) - prestart))s${ENDCOLOR}" 44 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.gradle.develocity") version ("3.18.2") 3 | } 4 | 5 | rootProject.name = "manifest-plugin" 6 | 7 | develocity { 8 | buildScan { 9 | termsOfUseUrl = "https://gradle.com/help/legal-terms-of-use" 10 | termsOfUseAgree = "yes" 11 | 12 | publishing.onlyIf { false } 13 | if (!System.getenv("CI").isNullOrEmpty()) { 14 | publishing.onlyIf { true } 15 | tag("CI") 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/integration/kotlin/com/coditory/gradle/integration/CommandLineAcceptanceTest.kt: -------------------------------------------------------------------------------- 1 | package com.coditory.gradle.integration 2 | 3 | import com.coditory.gradle.manifest.base.GradleTestVersions.GRADLE_MAX_SUPPORTED_VERSION 4 | import com.coditory.gradle.manifest.base.GradleTestVersions.GRADLE_MIN_SUPPORTED_VERSION 5 | import com.coditory.gradle.manifest.base.TestProjectBuilder 6 | import com.coditory.gradle.manifest.base.TestRepository.Companion.COMMIT_MESSAGE 7 | import org.assertj.core.api.Assertions.assertThat 8 | import org.gradle.testkit.runner.TaskOutcome 9 | import org.junit.jupiter.api.AfterEach 10 | import org.junit.jupiter.api.AutoClose 11 | import org.junit.jupiter.api.Test 12 | import org.junit.jupiter.params.ParameterizedTest 13 | import org.junit.jupiter.params.provider.ValueSource 14 | 15 | class CommandLineAcceptanceTest { 16 | companion object { 17 | @AutoClose 18 | private val project = TestProjectBuilder 19 | .project("project-" + CommandLineAcceptanceTest::class.simpleName) 20 | .withBuildGradleKts( 21 | """ 22 | plugins { 23 | id("java") 24 | id("application") 25 | id("com.coditory.manifest") 26 | } 27 | 28 | group = "com.coditory" 29 | version = "0.0.1-SNAPSHOT" 30 | 31 | repositories { 32 | mavenCentral() 33 | } 34 | 35 | application { 36 | mainClass.set("com.coditory.Application") 37 | } 38 | """.trimIndent(), 39 | ) 40 | .withFile( 41 | "src/main/java/com/coditory/Application.java", 42 | """ 43 | package com.coditory; 44 | 45 | public class Application { 46 | public static void main(String[] args) { 47 | System.out.println(">>> Application.main"); 48 | } 49 | } 50 | """.trimIndent(), 51 | ) 52 | .withGitRepository() 53 | .build() 54 | } 55 | 56 | @AfterEach 57 | fun cleanProject() { 58 | project.clean() 59 | } 60 | 61 | private val expectedManifestKeys = listOf( 62 | "Manifest-Version:", 63 | "Main-Class:", 64 | "Implementation-Title:", 65 | "Implementation-Group:", 66 | "Implementation-Version:", 67 | "Built-By:", 68 | "Built-Host:", 69 | "Built-Date:", 70 | "Built-OS:", 71 | "Built-JDK:", 72 | "SCM-Repository:", 73 | "SCM-Branch:", 74 | "SCM-Commit-Message:", 75 | "SCM-Commit-Hash:", 76 | "SCM-Commit-Author:", 77 | "SCM-Commit-Date:", 78 | ) 79 | 80 | @ParameterizedTest(name = "should generate manifest on processResources command for gradle {0}") 81 | @ValueSource(strings = [GRADLE_MAX_SUPPORTED_VERSION, GRADLE_MIN_SUPPORTED_VERSION]) 82 | fun `should generate manifest on processResources command`(gradleVersion: String?) { 83 | // when 84 | val result = project.runGradle(listOf("processResources"), gradleVersion) 85 | // then 86 | assertThat(result.task(":manifest")?.outcome) 87 | .isEqualTo(TaskOutcome.SUCCESS) 88 | // and 89 | assertThat(project.readFile("build/resources/main/META-INF/MANIFEST.MF")) 90 | .contains(expectedManifestKeys) 91 | .contains("Implementation-Title: ${project.name}") 92 | .contains("Implementation-Group: com.coditory") 93 | .contains("Implementation-Version: 0.0.1-SNAPSHOT") 94 | .contains("SCM-Commit-Message: $COMMIT_MESSAGE") 95 | .contains("Main-Class: com.coditory.Application") 96 | } 97 | 98 | @Test 99 | fun `should generate manifest in output directory on manifest command`() { 100 | // when 101 | val result = project.runGradle(listOf("manifest")) 102 | // then 103 | assertThat(result.task(":manifest")?.outcome) 104 | .isEqualTo(TaskOutcome.SUCCESS) 105 | // and 106 | assertThat(project.readFile("build/resources/main/META-INF/MANIFEST.MF")) 107 | .isNotEmpty() 108 | } 109 | 110 | @Test 111 | fun `should print out manifest on manifest command with --print flag`() { 112 | // when 113 | val result = project.runGradle(listOf("manifest", "--print")) 114 | // then 115 | assertThat(result.task(":manifest")?.outcome) 116 | .isEqualTo(TaskOutcome.SUCCESS) 117 | // and 118 | assertThat(result.output) 119 | .contains(expectedManifestKeys) 120 | } 121 | 122 | @Test 123 | fun `should work with build cache`() { 124 | // when 125 | val result = project.runGradle(listOf("manifest", "--build-cache")) 126 | // then 127 | assertThat(result.task(":manifest")?.outcome) 128 | .isEqualTo(TaskOutcome.SUCCESS) 129 | // and 130 | assertThat(project.readFile("build/resources/main/META-INF/MANIFEST.MF")) 131 | .isNotEmpty() 132 | } 133 | 134 | @Test 135 | fun `should work with configuration cache`() { 136 | // when 137 | val result = project.runGradle(listOf("manifest", "--configuration-cache")) 138 | // then 139 | assertThat(result.task(":manifest")?.outcome) 140 | .isEqualTo(TaskOutcome.SUCCESS) 141 | // and 142 | assertThat(project.readFile("build/resources/main/META-INF/MANIFEST.MF")) 143 | .isNotEmpty() 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/integration/kotlin/com/coditory/gradle/integration/GenerateClasspathTest.kt: -------------------------------------------------------------------------------- 1 | package com.coditory.gradle.integration 2 | 3 | import com.coditory.gradle.manifest.base.GradleTestVersions.GRADLE_MAX_SUPPORTED_VERSION 4 | import com.coditory.gradle.manifest.base.GradleTestVersions.GRADLE_MIN_SUPPORTED_VERSION 5 | import com.coditory.gradle.manifest.base.TestProject 6 | import com.coditory.gradle.manifest.base.TestProjectBuilder 7 | import com.coditory.gradle.manifest.base.Versions 8 | import org.assertj.core.api.Assertions.assertThat 9 | import org.gradle.testkit.runner.TaskOutcome 10 | import org.junit.jupiter.api.AfterAll 11 | import org.junit.jupiter.api.Test 12 | import org.junit.jupiter.params.ParameterizedTest 13 | import org.junit.jupiter.params.provider.ValueSource 14 | import java.util.regex.Pattern.DOTALL 15 | 16 | class GenerateClasspathTest { 17 | @ParameterizedTest(name = "should generate manifest with classpath for gradle {0}") 18 | @ValueSource(strings = [GRADLE_MAX_SUPPORTED_VERSION, GRADLE_MIN_SUPPORTED_VERSION]) 19 | fun `should generate classpath`(gradleVersion: String?) { 20 | // given 21 | val project = TestProjectBuilder 22 | .project("project-01-" + CommandLineAcceptanceTest::class.simpleName) 23 | .withBuildGradleKts( 24 | """ 25 | plugins { 26 | id("java") 27 | id("com.coditory.manifest") 28 | } 29 | 30 | repositories { 31 | mavenCentral() 32 | } 33 | 34 | group = "com.coditory" 35 | 36 | manifest { 37 | classpathPrefix = "my/jars" 38 | } 39 | 40 | dependencies { 41 | implementation("com.github.slugify:slugify:${Versions.slugify}") 42 | runtimeOnly("org.hashids:hashids:${Versions.hashids}") 43 | testImplementation("org.junit.jupiter:junit-jupiter-api:${Versions.junit}") 44 | } 45 | """, 46 | ) 47 | .build() 48 | deferCleanUp(project) 49 | 50 | // when 51 | val result = project.runGradle(listOf("processResources"), gradleVersion) 52 | 53 | // then 54 | assertThat(result.task(":manifest")?.outcome) 55 | .isEqualTo(TaskOutcome.SUCCESS) 56 | 57 | // and 58 | val manifest = project.readFile("build/resources/main/META-INF/MANIFEST.MF") 59 | assertThat(manifest.replace("\r\n ", "")) 60 | .contains("Class-Path: my/jars/slugify-${Versions.slugify}.jar my/jars/hashids-${Versions.hashids}.jar my/jars/icu4j-64.2.jar\r\n") 61 | .matches(".*Class-Path: [^:]+(Built-JDK: [^:]+)?$".toPattern(DOTALL)) 62 | } 63 | 64 | @Test 65 | fun `should create classpath with unix file separators from windows classpathPrefix`() { 66 | // given 67 | val project = TestProjectBuilder 68 | .project("project-02-" + CommandLineAcceptanceTest::class.simpleName) 69 | .withBuildGradleKts( 70 | """ 71 | plugins { 72 | id("java") 73 | id("com.coditory.manifest") 74 | } 75 | 76 | repositories { 77 | mavenCentral() 78 | } 79 | 80 | group = "com.coditory" 81 | 82 | manifest { 83 | classpathPrefix = "my\\important\\jars\\" 84 | } 85 | 86 | dependencies { 87 | implementation("com.github.slugify:slugify:${Versions.slugify}") 88 | } 89 | """, 90 | ) 91 | .build() 92 | deferCleanUp(project) 93 | 94 | // when 95 | val result = project.runGradle(listOf("processResources")) 96 | 97 | // then 98 | assertThat(result.task(":manifest")?.outcome) 99 | .isEqualTo(TaskOutcome.SUCCESS) 100 | 101 | // and 102 | val manifest = project.readFile("build/resources/main/META-INF/MANIFEST.MF") 103 | assertThat(manifest.replace("\r\n ", "")) 104 | .contains("my/important/jars/slugify-${Versions.slugify}.jar") 105 | } 106 | 107 | companion object { 108 | private val projects: MutableList = mutableListOf() 109 | 110 | @Synchronized 111 | fun deferCleanUp(project: TestProject) { 112 | projects.add(project) 113 | } 114 | 115 | @AfterAll 116 | @JvmStatic 117 | fun cleanUp() { 118 | projects.forEach { it.close() } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/integration/kotlin/com/coditory/gradle/integration/GenerateWithCustomAttributesTest.kt: -------------------------------------------------------------------------------- 1 | package com.coditory.gradle.integration 2 | 3 | import com.coditory.gradle.manifest.base.GradleTestVersions.GRADLE_MAX_SUPPORTED_VERSION 4 | import com.coditory.gradle.manifest.base.GradleTestVersions.GRADLE_MIN_SUPPORTED_VERSION 5 | import com.coditory.gradle.manifest.base.TestProject 6 | import com.coditory.gradle.manifest.base.TestProjectBuilder 7 | import com.coditory.gradle.manifest.base.Versions 8 | import org.assertj.core.api.Assertions.assertThat 9 | import org.gradle.testkit.runner.TaskOutcome 10 | import org.junit.jupiter.api.AfterAll 11 | import org.junit.jupiter.params.ParameterizedTest 12 | import org.junit.jupiter.params.provider.ValueSource 13 | import kotlin.collections.forEach 14 | 15 | class GenerateWithCustomAttributesTest { 16 | @ParameterizedTest(name = "should generate manifest with custom attributes for gradle {0}") 17 | @ValueSource(strings = [GRADLE_MAX_SUPPORTED_VERSION, GRADLE_MIN_SUPPORTED_VERSION]) 18 | fun `should generate manifest with custom attributes`(gradleVersion: String?) { 19 | // given 20 | val project = TestProjectBuilder 21 | .project("project-" + GenerateWithCustomAttributesTest::class.simpleName) 22 | .withBuildGradleKts( 23 | """ 24 | plugins { 25 | id("java") 26 | id("com.coditory.manifest") 27 | } 28 | 29 | repositories { 30 | mavenCentral() 31 | } 32 | 33 | group = "com.coditory" 34 | version = "0.0.1-SNAPSHOT" 35 | 36 | manifest { 37 | buildAttributes = false 38 | implementationAttributes = true 39 | scmAttributes = false 40 | attributes = mapOf( 41 | "Custom-1" to "Custom-Value", 42 | "Custom-2" to 123, 43 | ) 44 | } 45 | 46 | dependencies { 47 | compileOnly("org.springframework.boot:spring-boot-starter:${Versions.spring}") 48 | testImplementation("org.junit.jupiter:junit-jupiter-api:${Versions.junit}") 49 | } 50 | """, 51 | ) 52 | .build() 53 | deferCleanUp(project) 54 | 55 | // when 56 | val result = project.runGradle(listOf("processResources"), gradleVersion) 57 | 58 | // then 59 | assertThat(result.task(":manifest")?.outcome) 60 | .isEqualTo(TaskOutcome.SUCCESS) 61 | 62 | // and 63 | assertThat(project.readFile("build/resources/main/META-INF/MANIFEST.MF")) 64 | .contains("Implementation-Title: ${project.name}") 65 | .contains("Implementation-Group: com.coditory") 66 | .contains("Implementation-Version: 0.0.1-SNAPSHOT") 67 | .contains("Custom-1: Custom-Value") 68 | .contains("Custom-2: 123") 69 | .doesNotContain("Main-Class") 70 | .doesNotContain("Built-") 71 | .doesNotContain("SCM-") 72 | } 73 | 74 | companion object { 75 | private val projects: MutableList = mutableListOf() 76 | 77 | @Synchronized 78 | fun deferCleanUp(project: TestProject) { 79 | projects.add(project) 80 | } 81 | 82 | @AfterAll 83 | @JvmStatic 84 | fun cleanUp() { 85 | projects.forEach { it.close() } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/integration/kotlin/com/coditory/gradle/integration/MultiModuleAcceptanceTest.kt: -------------------------------------------------------------------------------- 1 | package com.coditory.gradle.integration 2 | 3 | import com.coditory.gradle.manifest.base.TestProjectBuilder 4 | import com.coditory.gradle.manifest.base.TestRepository.Companion.COMMIT_MESSAGE 5 | import org.assertj.core.api.Assertions.assertThat 6 | import org.junit.jupiter.api.AfterEach 7 | import org.junit.jupiter.api.Test 8 | 9 | class MultiModuleAcceptanceTest { 10 | companion object { 11 | private val parentProject = TestProjectBuilder 12 | .project("parent-project") 13 | .withBuildGradleKts( 14 | """ 15 | plugins { 16 | id("com.coditory.manifest") apply false 17 | } 18 | 19 | allprojects { 20 | group = "com.coditory.sandbox" 21 | version = "0.0.1-SNAPSHOT" 22 | } 23 | 24 | subprojects { 25 | apply(plugin = "com.coditory.manifest") 26 | } 27 | """.trimIndent(), 28 | ) 29 | .withFile( 30 | "settings.gradle.kts", 31 | """ 32 | rootProject.name = "parent-project" 33 | include("project-a") 34 | include("project-b") 35 | """.trimIndent(), 36 | ) 37 | .withGitRepository() 38 | .build() 39 | 40 | private val projectA = TestProjectBuilder 41 | .project("project-a", parentProject.projectDir.resolve("project-a")) 42 | .withBuildGradle( 43 | """ 44 | plugins { 45 | id("java") 46 | id("application") 47 | } 48 | 49 | repositories { 50 | mavenCentral() 51 | } 52 | 53 | application { 54 | mainClass.set("com.coditory.ProjectA") 55 | } 56 | """.trimIndent(), 57 | ) 58 | .withFile( 59 | "src/main/java/com/coditory/ProjectA.java", 60 | """ 61 | package com.coditory; 62 | 63 | public class ProjectA { 64 | public static void main(String[] args) { 65 | System.out.println(">>> ProjectA"); 66 | } 67 | } 68 | """.trimIndent(), 69 | ) 70 | .build() 71 | 72 | private val projectB = TestProjectBuilder.project("project-b", parentProject.projectDir.resolve("project-b")) 73 | .withBuildGradle( 74 | """ 75 | plugins { 76 | id("java") 77 | id("application") 78 | } 79 | 80 | repositories { 81 | mavenCentral() 82 | } 83 | 84 | application { 85 | mainClass.set("com.coditory.ProjectB") 86 | } 87 | """.trimIndent(), 88 | ) 89 | .withFile( 90 | "src/main/java/com/coditory/ProjectB.java", 91 | """ 92 | package com.coditory; 93 | 94 | public class ProjectB { 95 | public static void main(String[] args) { 96 | System.out.println(">>> ProjectB"); 97 | } 98 | } 99 | """.trimIndent(), 100 | ) 101 | .build() 102 | } 103 | 104 | @AfterEach 105 | fun cleanProject() { 106 | parentProject.clean() 107 | } 108 | 109 | private val expectedManifestKeys = listOf( 110 | "Manifest-Version:", 111 | "Main-Class:", 112 | "Implementation-Title:", 113 | "Implementation-Group:", 114 | "Implementation-Version:", 115 | "Built-By:", 116 | "Built-Host:", 117 | "Built-Date:", 118 | "Built-OS:", 119 | "Built-JDK:", 120 | "SCM-Repository:", 121 | "SCM-Branch:", 122 | "SCM-Commit-Message:", 123 | "SCM-Commit-Hash:", 124 | "SCM-Commit-Author:", 125 | "SCM-Commit-Date:", 126 | ) 127 | 128 | @Test 129 | fun `should generate manifest on processResources command in sub project`() { 130 | // when 131 | parentProject.runGradle(listOf("processResources")) 132 | 133 | // then 134 | assertThat(projectA.readFile("build/resources/main/META-INF/MANIFEST.MF")) 135 | .contains(expectedManifestKeys) 136 | .contains("Implementation-Title: project-a") 137 | .contains("Implementation-Group: com.coditory") 138 | .contains("Implementation-Version: 0.0.1-SNAPSHOT") 139 | .contains("Main-Class: com.coditory.ProjectA") 140 | .contains("SCM-Commit-Message: $COMMIT_MESSAGE") 141 | 142 | // and 143 | assertThat(projectB.readFile("build/resources/main/META-INF/MANIFEST.MF")) 144 | .contains(expectedManifestKeys) 145 | .contains("Implementation-Title: project-b") 146 | .contains("Implementation-Group: com.coditory") 147 | .contains("Implementation-Version: 0.0.1-SNAPSHOT") 148 | .contains("Main-Class: com.coditory.ProjectB") 149 | .contains("SCM-Commit-Message: $COMMIT_MESSAGE") 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/main/kotlin/com/coditory/gradle/manifest/ManifestParameterizedPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.coditory.gradle.manifest 2 | 3 | import com.coditory.gradle.manifest.ManifestPlugin.Companion.GENERATE_MANIFEST_TASK 4 | import com.coditory.gradle.manifest.ManifestPlugin.Companion.MANIFEST_EXTENSION 5 | import com.coditory.gradle.manifest.attributes.HostNameResolver 6 | import com.coditory.gradle.manifest.attributes.ManifestAttributeResolver.fillManifestAttributes 7 | import org.gradle.api.Plugin 8 | import org.gradle.api.Project 9 | import org.gradle.api.plugins.JavaPlugin 10 | import org.gradle.language.base.plugins.LifecycleBasePlugin.BUILD_GROUP 11 | import java.time.Clock 12 | 13 | open class ManifestParameterizedPlugin( 14 | private val clock: Clock, 15 | private val hostNameResolver: HostNameResolver, 16 | ) : Plugin { 17 | 18 | override fun apply(project: Project) { 19 | if (!project.plugins.hasPlugin(JavaPlugin::class.java)) { 20 | project.plugins.apply(JavaPlugin::class.java) 21 | } 22 | setupExtension(project) 23 | fillManifestAttributes(clock, hostNameResolver, project) 24 | setupGenerateManifestTask(project) 25 | } 26 | 27 | private fun setupExtension(project: Project) { 28 | project.extensions.create(MANIFEST_EXTENSION, ManifestPluginExtension::class.java) 29 | } 30 | 31 | private fun setupGenerateManifestTask(project: Project) { 32 | val manifestTask = project.tasks.register(GENERATE_MANIFEST_TASK, ManifestTask::class.java) { 33 | it.description = "Generates META-INF/MANIFEST.MF in resources directory with project metadata" 34 | it.group = BUILD_GROUP 35 | } 36 | project.tasks.getByName(JavaPlugin.PROCESS_RESOURCES_TASK_NAME) 37 | .finalizedBy(manifestTask) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/kotlin/com/coditory/gradle/manifest/ManifestPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.coditory.gradle.manifest 2 | 3 | import com.coditory.gradle.manifest.attributes.HostNameResolver.Companion.INET_HOST_NAME_RESOLVER 4 | import java.time.Clock 5 | 6 | open class ManifestPlugin : ManifestParameterizedPlugin( 7 | clock = Clock.systemUTC(), 8 | hostNameResolver = INET_HOST_NAME_RESOLVER, 9 | ) { 10 | companion object { 11 | const val PLUGIN_ID = "com.coditory.manifest" 12 | const val MANIFEST_EXTENSION = "manifest" 13 | const val GENERATE_MANIFEST_TASK = "manifest" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/kotlin/com/coditory/gradle/manifest/ManifestPluginExtension.kt: -------------------------------------------------------------------------------- 1 | package com.coditory.gradle.manifest 2 | 3 | open class ManifestPluginExtension { 4 | var classpathPrefix: String? = null 5 | var attributes: Map? = null 6 | var implementationAttributes: Boolean = true 7 | var buildAttributes: Boolean = true 8 | var scmAttributes: Boolean = true 9 | } 10 | -------------------------------------------------------------------------------- /src/main/kotlin/com/coditory/gradle/manifest/ManifestTask.kt: -------------------------------------------------------------------------------- 1 | package com.coditory.gradle.manifest 2 | 3 | import org.gradle.api.DefaultTask 4 | import org.gradle.api.plugins.JavaPlugin.JAR_TASK_NAME 5 | import org.gradle.api.plugins.JavaPluginExtension 6 | import org.gradle.api.tasks.Input 7 | import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME 8 | import org.gradle.api.tasks.TaskAction 9 | import org.gradle.api.tasks.options.Option 10 | import org.gradle.jvm.tasks.Jar 11 | import java.nio.file.Path 12 | 13 | open class ManifestTask : DefaultTask() { 14 | private val manifest = project.tasks 15 | .named(JAR_TASK_NAME, Jar::class.java).get() 16 | .manifest 17 | private val outputPath = project.extensions.getByType(JavaPluginExtension::class.java) 18 | .sourceSets 19 | .getByName(MAIN_SOURCE_SET_NAME) 20 | .output.resourcesDir?.toPath() 21 | private val srcMainPath = project.extensions.getByType(JavaPluginExtension::class.java) 22 | .sourceSets 23 | .getByName(MAIN_SOURCE_SET_NAME) 24 | .resources.srcDirs 25 | .firstOrNull()?.toPath() 26 | 27 | private var print: Boolean = false 28 | private var main: Boolean = false 29 | 30 | @Option(option = "print", description = "Prints out the manifest.") 31 | open fun setPrint(print: Boolean = true) { 32 | this.print = print 33 | } 34 | 35 | @Input 36 | open fun getPrint(): Boolean { 37 | return print 38 | } 39 | 40 | @Option(option = "main", description = "Writes manifest to src/main/resources.") 41 | open fun setMain(main: Boolean = true) { 42 | this.main = main 43 | } 44 | 45 | @Input 46 | open fun getMain(): Boolean { 47 | return main 48 | } 49 | 50 | @TaskAction 51 | fun generateManifest() { 52 | if (print) { 53 | printManifest() 54 | } 55 | if (main) { 56 | generateManifestToResources() 57 | } else { 58 | generateManifestToOutput() 59 | } 60 | } 61 | 62 | private fun printManifest() { 63 | println("") 64 | println("MANIFEST.MF") 65 | println("===========") 66 | manifest.attributes.forEach { 67 | println(it.key + ": " + it.value) 68 | } 69 | println("") 70 | } 71 | 72 | private fun generateManifestToOutput() { 73 | if (outputPath != null) { 74 | writeManifest(outputPath) 75 | } 76 | } 77 | 78 | private fun generateManifestToResources() { 79 | if (srcMainPath != null) { 80 | writeManifest(srcMainPath) 81 | } 82 | } 83 | 84 | private fun writeManifest(resourcePath: Path) { 85 | manifest.writeTo(resourcePath.resolve(MANIFEST_PATH)) 86 | } 87 | 88 | companion object { 89 | private const val MANIFEST_PATH = "META-INF/MANIFEST.MF" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/kotlin/com/coditory/gradle/manifest/attributes/HostNameResolver.kt: -------------------------------------------------------------------------------- 1 | package com.coditory.gradle.manifest.attributes 2 | 3 | import java.net.InetAddress 4 | 5 | interface HostNameResolver { 6 | fun resolveHostName(): String 7 | 8 | companion object { 9 | val INET_HOST_NAME_RESOLVER = object : HostNameResolver { 10 | override fun resolveHostName(): String { 11 | return InetAddress.getLocalHost().hostName 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/kotlin/com/coditory/gradle/manifest/attributes/ManifestAttributeResolver.kt: -------------------------------------------------------------------------------- 1 | package com.coditory.gradle.manifest.attributes 2 | 3 | import com.coditory.gradle.manifest.ManifestPluginExtension 4 | import org.gradle.api.Project 5 | import org.gradle.api.plugins.BasePluginExtension 6 | import org.gradle.api.plugins.JavaApplication 7 | import org.gradle.api.plugins.JavaPlugin.JAR_TASK_NAME 8 | import org.gradle.api.plugins.JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME 9 | import org.gradle.jvm.tasks.Jar 10 | import java.nio.file.Paths 11 | import java.time.Clock 12 | import java.time.Instant 13 | import java.time.temporal.ChronoUnit.SECONDS 14 | 15 | internal object ManifestAttributeResolver { 16 | fun fillManifestAttributes(clock: Clock, hostNameResolver: HostNameResolver, project: Project) { 17 | project.tasks.named(JAR_TASK_NAME, Jar::class.java) { 18 | val attributes = project.tasks 19 | .named(JAR_TASK_NAME, Jar::class.java).get() 20 | .manifest.attributes 21 | val generated = resolveAttributes(clock, hostNameResolver, project) 22 | .filter { !attributes.containsKey(it.first) } 23 | attributes.putAll(generated) 24 | } 25 | } 26 | 27 | private fun resolveAttributes( 28 | clock: Clock, 29 | hostNameResolver: HostNameResolver, 30 | project: Project, 31 | ): List> { 32 | val extension = project.extensions.getByType(ManifestPluginExtension::class.java) 33 | return mainClassAttribute(project) 34 | .plus(implementationAttributes(project, extension)) 35 | .plus(buildAttributes(clock, hostNameResolver, extension)) 36 | .plus(scmAttributes(project, extension)) 37 | .plus(customAttributes(extension)) 38 | .plus(classpathAttribute(project, extension)) 39 | .map { it.key to it.value?.toString()?.trim() } 40 | .filter { !it.second.isNullOrBlank() } 41 | } 42 | 43 | private fun customAttributes(extension: ManifestPluginExtension): Map { 44 | val attributes = extension.attributes 45 | if (attributes.isNullOrEmpty()) { 46 | return mapOf() 47 | } 48 | return attributes.entries 49 | .filter { it.key.isNotBlank() && it.value != null } 50 | .associate { it.key to orEmpty { it.value } } 51 | } 52 | 53 | private fun classpathAttribute(project: Project, extension: ManifestPluginExtension): Map { 54 | val classpathPrefix = extension.classpathPrefix ?: return mapOf() 55 | val classPath = project.configurations.getByName(RUNTIME_CLASSPATH_CONFIGURATION_NAME) 56 | .map { Paths.get(classpathPrefix, it.name).toString() } 57 | .joinToString(" ") { it.replace('\\', '/').replace("//+".toRegex(), "/") } 58 | return mapOf("Class-Path" to classPath) 59 | } 60 | 61 | private fun implementationTitle(project: Project): String { 62 | return project.extensions.getByType(BasePluginExtension::class.java) 63 | .archivesName.get() 64 | } 65 | 66 | private fun systemProperties(vararg names: String): String { 67 | return names 68 | .map { System.getProperty(it)?.trim() } 69 | .filter { !it.isNullOrBlank() } 70 | .joinToString(" ") 71 | } 72 | 73 | private fun mainClassAttribute(project: Project): Map { 74 | val javaApplication = project.extensions.findByType(JavaApplication::class.java) 75 | return mapOf( 76 | "Main-Class" to orEmpty { javaApplication?.mainClass?.orNull }, 77 | ) 78 | } 79 | 80 | private fun implementationAttributes( 81 | project: Project, 82 | extension: ManifestPluginExtension, 83 | ): Map { 84 | if (!extension.implementationAttributes) { 85 | return mapOf() 86 | } 87 | return mapOf( 88 | "Implementation-Title" to lazy { implementationTitle(project) }, 89 | "Implementation-Group" to lazy { project.group }, 90 | "Implementation-Version" to lazy { project.version }, 91 | ) 92 | } 93 | 94 | private fun buildAttributes( 95 | clock: Clock, 96 | hostNameResolver: HostNameResolver, 97 | extension: ManifestPluginExtension, 98 | ): Map { 99 | if (!extension.buildAttributes) { 100 | return mapOf() 101 | } 102 | return mapOf( 103 | "Built-By" to systemProperties("user.name"), 104 | "Built-Host" to orEmpty { hostNameResolver.resolveHostName() }, 105 | "Built-Date" to format(clock.instant()), 106 | "Built-OS" to systemProperties("os.name", "os.version", "os.arch"), 107 | "Built-JDK" to systemProperties("java.version", "java.vendor"), 108 | ) 109 | } 110 | 111 | private fun scmAttributes(project: Project, extension: ManifestPluginExtension): Map { 112 | if (!extension.scmAttributes) { 113 | return mapOf() 114 | } 115 | val repository = project.providers.of(RepositoryValueSource::class.java) { config -> 116 | config.parameters { it.projectDir.set(project.rootProject.projectDir) } 117 | }.get() 118 | return mapOf( 119 | "SCM-Repository" to orEmpty { repository.url }, 120 | "SCM-Branch" to orEmpty { repository.branch }, 121 | "SCM-Commit-Message" to orEmpty { repository.commitMessage }, 122 | "SCM-Commit-Hash" to orEmpty { repository.commitHash }, 123 | "SCM-Commit-Author" to orEmpty { 124 | repository.commitAuthorName?.let { 125 | if (repository.commitAuthorEmail != null) { 126 | "${repository.commitAuthorName} <${repository.commitAuthorEmail}>" 127 | } else { 128 | repository.commitAuthorName 129 | } 130 | } 131 | }, 132 | "SCM-Commit-Date" to orEmpty { repository.commitEpochSeconds?.let { format(Instant.ofEpochSecond(it)) } }, 133 | ) 134 | } 135 | 136 | private fun format(instant: Instant): String { 137 | return instant.truncatedTo(SECONDS).toString() 138 | } 139 | 140 | private fun lazy(provider: () -> Any?): Any { 141 | return object { 142 | override fun toString(): String { 143 | return orEmpty(provider) 144 | } 145 | } 146 | } 147 | 148 | private fun orEmpty(provider: () -> Any?): String { 149 | return try { 150 | provider()?.toString() ?: "" 151 | } catch (_: Throwable) { 152 | "" 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/main/kotlin/com/coditory/gradle/manifest/attributes/RepositoryValueSource.kt: -------------------------------------------------------------------------------- 1 | package com.coditory.gradle.manifest.attributes 2 | 3 | import org.eclipse.jgit.api.Git 4 | import org.eclipse.jgit.lib.Constants.HEAD 5 | import org.gradle.api.provider.Property 6 | import org.gradle.api.provider.ValueSource 7 | import org.gradle.api.provider.ValueSourceParameters 8 | import java.io.File 9 | 10 | data class Repostiory( 11 | val url: String? = null, 12 | val branch: String? = null, 13 | val commitMessage: String? = null, 14 | val commitHash: String? = null, 15 | val commitAuthorName: String? = null, 16 | val commitAuthorEmail: String? = null, 17 | val commitEpochSeconds: Long? = null, 18 | ) { 19 | companion object { 20 | private val EMPTY = Repostiory() 21 | 22 | fun empty() = EMPTY 23 | } 24 | } 25 | 26 | abstract class RepositoryValueSource : ValueSource { 27 | interface Params : ValueSourceParameters { 28 | val projectDir: Property 29 | } 30 | 31 | override fun obtain(): Repostiory { 32 | return try { 33 | val repository = Git.open(parameters.projectDir.get()).repository 34 | val head = repository.parseCommit(repository.resolve(HEAD)) 35 | return Repostiory( 36 | url = repository.config.getString("remote", "origin", "url"), 37 | branch = repository.fullBranch, 38 | commitMessage = head.shortMessage, 39 | commitHash = head.name(), 40 | commitAuthorName = head.authorIdent.name.trim(), 41 | commitAuthorEmail = head.authorIdent.emailAddress.trim(), 42 | commitEpochSeconds = head.authorIdent.whenAsInstant.epochSecond, 43 | ) 44 | } catch (_: Throwable) { 45 | // passing logger via valueSource parameters does not work 46 | Repostiory.empty() 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/kotlin/com/coditory/gradle/manifest/GenerateManifestTest.kt: -------------------------------------------------------------------------------- 1 | package com.coditory.gradle.manifest 2 | 3 | import com.coditory.gradle.manifest.base.SystemOutputCapturer.Companion.captureSystemOutput 4 | import com.coditory.gradle.manifest.base.TestProjectBuilder 5 | import com.coditory.gradle.manifest.base.TestProjectBuilder.Companion.projectWithPlugins 6 | import org.assertj.core.api.Assertions.assertThat 7 | import org.junit.jupiter.api.Test 8 | 9 | class GenerateManifestTest { 10 | private val expectedManifestKeys = listOf( 11 | "Manifest-Version:", 12 | "Main-Class:", 13 | "Implementation-Title:", 14 | "Implementation-Group:", 15 | "Implementation-Version:", 16 | "Built-By:", 17 | "Built-Host:", 18 | "Built-Date:", 19 | "Built-OS:", 20 | "Built-JDK:", 21 | "SCM-Repository:", 22 | "SCM-Branch:", 23 | "SCM-Commit-Message:", 24 | "SCM-Commit-Hash:", 25 | "SCM-Commit-Author:", 26 | "SCM-Commit-Date:", 27 | ) 28 | 29 | @Test 30 | fun `should generate manifest to output`() { 31 | // given 32 | val project = projectBuilder().build() 33 | val manifestTask = project.getManifestTask() 34 | 35 | // when 36 | manifestTask.generateManifest() 37 | 38 | // then 39 | assertThat(project.readFile("build/resources/main/META-INF/MANIFEST.MF")) 40 | .contains(expectedManifestKeys) 41 | } 42 | 43 | @Test 44 | fun `should generate manifest to output and idea output when idea project is detected`() { 45 | // given 46 | val project = projectBuilder() 47 | .withIdeaProjectFiles() 48 | .build() 49 | val manifestTask = project.getManifestTask() 50 | 51 | // when 52 | manifestTask.generateManifest() 53 | 54 | // then 55 | assertThat(project.readFile("build/resources/main/META-INF/MANIFEST.MF")) 56 | .contains(expectedManifestKeys) 57 | } 58 | 59 | @Test 60 | fun `should generate manifest to src-main-resources on --main option`() { 61 | // given 62 | val project = projectBuilder().build() 63 | val manifestTask = project.getManifestTask() 64 | manifestTask.setMain() 65 | 66 | // when 67 | manifestTask.generateManifest() 68 | 69 | // then 70 | assertThat(project.readFile("src/main/resources/META-INF/MANIFEST.MF")) 71 | .contains(expectedManifestKeys) 72 | } 73 | 74 | @Test 75 | fun `should generate manifest and print it to stdout on --print option`() { 76 | // given 77 | val project = projectBuilder().build() 78 | val manifestTask = project.getManifestTask() 79 | manifestTask.setPrint(true) 80 | 81 | // when 82 | val output = captureSystemOutput() 83 | .use { 84 | manifestTask.generateManifest() 85 | it.readSystemOut() 86 | } 87 | 88 | // then 89 | project.readFile("build/resources/main/META-INF/MANIFEST.MF") 90 | .isNotEmpty() 91 | 92 | // and 93 | assertThat(output).contains(expectedManifestKeys) 94 | } 95 | 96 | private fun projectBuilder(): TestProjectBuilder { 97 | return projectWithPlugins("sample-project") 98 | .withGroup("com.coditory") 99 | .withGitRepository() 100 | .withMainClass("com.coditory.MainClass") 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/test/kotlin/com/coditory/gradle/manifest/GenerateManifestWithoutDisabledAttributesTest.kt: -------------------------------------------------------------------------------- 1 | package com.coditory.gradle.manifest 2 | 3 | import com.coditory.gradle.manifest.base.TestProject 4 | import com.coditory.gradle.manifest.base.TestProjectBuilder 5 | import org.assertj.core.api.Assertions.assertThat 6 | import org.junit.jupiter.api.Test 7 | 8 | class GenerateManifestWithoutDisabledAttributesTest { 9 | @Test 10 | fun `should generate manifest without scm attributes`() { 11 | // when 12 | val manifest = project().generateManifest { it.scmAttributes = false } 13 | // then 14 | assertThat(manifest) 15 | .contains("Implementation-") 16 | .contains("Built-") 17 | .doesNotContain("SCM-") 18 | } 19 | 20 | @Test 21 | fun `should generate manifest without build attributes`() { 22 | // when 23 | val manifest = project().generateManifest { it.buildAttributes = false } 24 | // then 25 | assertThat(manifest) 26 | .contains("Implementation-") 27 | .doesNotContain("Built-") 28 | .contains("SCM-") 29 | } 30 | 31 | @Test 32 | fun `should generate manifest without implementation attributes`() { 33 | // when 34 | val manifest = project().generateManifest { it.implementationAttributes = false } 35 | // then 36 | assertThat(manifest) 37 | .doesNotContain("Implementation-") 38 | .contains("Built-") 39 | .contains("SCM-") 40 | } 41 | 42 | private fun project(): TestProject { 43 | return TestProjectBuilder.projectWithPlugins() 44 | .withGroup("com.coditory") 45 | .withGitRepository() 46 | .withMainClass("com.coditory.MainClass") 47 | .build() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/kotlin/com/coditory/gradle/manifest/PluginSetupTest.kt: -------------------------------------------------------------------------------- 1 | package com.coditory.gradle.manifest 2 | 3 | import com.coditory.gradle.manifest.ManifestPlugin.Companion.GENERATE_MANIFEST_TASK 4 | import com.coditory.gradle.manifest.base.TestProjectBuilder.Companion.projectWithPlugins 5 | import org.assertj.core.api.Assertions.assertThat 6 | import org.gradle.api.plugins.JavaPlugin 7 | import org.gradle.language.jvm.tasks.ProcessResources 8 | import org.junit.jupiter.api.Test 9 | 10 | class PluginSetupTest { 11 | private val project = projectWithPlugins() 12 | .build() 13 | 14 | @Test 15 | fun `should register manifest task to run after processResources`() { 16 | val task = project.tasks.getByName(GENERATE_MANIFEST_TASK) 17 | val processResources = project.tasks 18 | .named(JavaPlugin.PROCESS_RESOURCES_TASK_NAME, ProcessResources::class.java).get() 19 | assertThat(processResources.finalizedBy.getDependencies(processResources)) 20 | .contains(task) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/kotlin/com/coditory/gradle/manifest/SetupManifestAttributesTest.kt: -------------------------------------------------------------------------------- 1 | package com.coditory.gradle.manifest 2 | 3 | import com.coditory.gradle.manifest.base.ManifestExtractor.extractManifestAttributes 4 | import com.coditory.gradle.manifest.base.ManifestExtractor.extractManifestMap 5 | import com.coditory.gradle.manifest.base.SystemProperties.withSystemProperties 6 | import com.coditory.gradle.manifest.base.SystemProperties.withoutSystemProperties 7 | import com.coditory.gradle.manifest.base.TestProjectBuilder.Companion.projectWithPlugins 8 | import com.coditory.gradle.manifest.base.TestRepository.Companion.repository 9 | import org.assertj.core.api.Assertions.assertThat 10 | import org.gradle.api.plugins.JavaApplication 11 | import org.junit.jupiter.api.Test 12 | 13 | class SetupManifestAttributesTest { 14 | @Test 15 | fun `should generate basic attributes for bare bone project with no git and no system properties`() { 16 | // given 17 | val project = projectWithPlugins("sample-project") 18 | .build() 19 | 20 | // when 21 | val manifest = withoutSystemProperties { 22 | extractManifestMap(project) 23 | } 24 | 25 | // then 26 | assertThat(manifest).isEqualTo( 27 | mapOf( 28 | "Built-Date" to "2015-12-03T10:15:30Z", 29 | "Built-Host" to "localhost", 30 | "Implementation-Title" to "sample-project", 31 | "Implementation-Version" to "unspecified", 32 | "Manifest-Version" to "1.0", 33 | ), 34 | ) 35 | } 36 | 37 | @Test 38 | fun `should trim attribute values`() { 39 | // given 40 | val project = projectWithPlugins("sample-project") 41 | .build() 42 | val properties = mapOf( 43 | "user.name" to " john.doe ", 44 | "java.version" to "\n11.0.6\t\n", 45 | "java.vendor" to " \n\t AdoptOpenJDK\n\t ", 46 | ) 47 | 48 | // when 49 | val manifest = withSystemProperties(properties) { 50 | extractManifestMap(project) 51 | } 52 | 53 | // then 54 | assertThat(manifest["Built-By"]).isEqualTo("john.doe") 55 | assertThat(manifest["Built-JDK"]).isEqualTo("11.0.6 AdoptOpenJDK") 56 | } 57 | 58 | @Test 59 | fun `should generate attributes from project and system properties`() { 60 | // given 61 | val project = projectWithPlugins("sample-project") 62 | .withGroup("com.coditory") 63 | .withVersion("0.0.1-SNAPSHOT") 64 | .withMainClass("com.coditory.MainClass") 65 | .build() 66 | val properties = mapOf( 67 | "user.name" to "john.doe", 68 | "java.version" to "11.0.6", 69 | "java.vendor" to "AdoptOpenJDK", 70 | "os.arch" to "amd64", 71 | "os.name" to "Linux", 72 | "os.version" to "4.15.0-91-generic", 73 | ) 74 | 75 | // when 76 | val manifest = withSystemProperties(properties) { 77 | extractManifestMap(project) 78 | } 79 | 80 | // then 81 | assertThat(manifest).isEqualTo( 82 | mapOf( 83 | "Built-Date" to "2015-12-03T10:15:30Z", 84 | "Built-Host" to "localhost", 85 | "Built-By" to "john.doe", 86 | "Built-JDK" to "11.0.6 AdoptOpenJDK", 87 | "Built-OS" to "Linux 4.15.0-91-generic amd64", 88 | "Implementation-Group" to "com.coditory", 89 | "Implementation-Title" to "sample-project", 90 | "Implementation-Version" to "0.0.1-SNAPSHOT", 91 | "Main-Class" to "com.coditory.MainClass", 92 | "Manifest-Version" to "1.0", 93 | ), 94 | ) 95 | } 96 | 97 | @Test 98 | fun `should skip empty properties`() { 99 | // given 100 | val project = projectWithPlugins("sample-project") 101 | .withGroup("") 102 | .withVersion("") 103 | .withMainClass("") 104 | .build() 105 | val properties = mapOf( 106 | "user.name" to "", 107 | "java.version" to "", 108 | "java.vendor" to "", 109 | "os.arch" to "", 110 | "os.name" to "", 111 | "os.version" to "", 112 | ) 113 | 114 | // when 115 | val manifest = withSystemProperties(properties) { 116 | extractManifestMap(project) 117 | } 118 | 119 | // then 120 | assertThat(manifest).isEqualTo( 121 | mapOf( 122 | "Built-Date" to "2015-12-03T10:15:30Z", 123 | "Built-Host" to "localhost", 124 | "Implementation-Title" to "sample-project", 125 | "Manifest-Version" to "1.0", 126 | ), 127 | ) 128 | } 129 | 130 | @Test 131 | fun `should format partially empty properties`() { 132 | // given 133 | val project = projectWithPlugins("sample-project") 134 | .build() 135 | val properties = mapOf( 136 | "java.version" to "11.0.6", 137 | "java.vendor" to "", 138 | "os.arch" to "", 139 | "os.name" to "Linux", 140 | "os.version" to "", 141 | ) 142 | 143 | // when 144 | val manifest = withSystemProperties(properties) { 145 | extractManifestMap(project) 146 | } 147 | 148 | // then 149 | assertThat(manifest) 150 | .containsEntry("Built-OS", "Linux") 151 | .containsEntry("Built-JDK", "11.0.6") 152 | } 153 | 154 | @Test 155 | fun `should generate attributes from git metadata`() { 156 | // given 157 | val project = projectWithPlugins("sample-project") 158 | .withGroup("com.coditory") 159 | .withVersion("0.0.1-SNAPSHOT") 160 | .build() 161 | val repo = repository(project) 162 | .withRemote() 163 | .withCommit() 164 | 165 | // when 166 | val scmManifest = extractManifestMap(project) 167 | .filter { it.key.startsWith("SCM-") } 168 | .toMap() 169 | 170 | // then 171 | assertThat(scmManifest).isEqualTo( 172 | mapOf( 173 | "SCM-Branch" to "refs/heads/main", 174 | "SCM-Commit-Message" to "Very important commit", 175 | "SCM-Commit-Author" to "John Doe ", 176 | "SCM-Commit-Date" to "2020-03-24T19:46:03Z", 177 | "SCM-Commit-Hash" to repo.getLastCommit().name(), 178 | "SCM-Repository" to "git@github.com:coditory/gradle-manifest-plugin.git", 179 | ), 180 | ) 181 | } 182 | 183 | @Test 184 | fun `should skip git attributes when nothing was committed`() { 185 | // given 186 | val project = projectWithPlugins("sample-project") 187 | .withGroup("com.coditory") 188 | .withVersion("0.0.1-SNAPSHOT") 189 | .build() 190 | repository(project) 191 | 192 | // when 193 | val scmManifest = extractManifestMap(project) 194 | .filter { it.key.startsWith("SCM-") } 195 | .toMap() 196 | 197 | // then 198 | assertThat(scmManifest).isEmpty() 199 | } 200 | 201 | @Test 202 | fun `should allow overwriting manifest attributes by user`() { 203 | // given 204 | val project = projectWithPlugins("sample-project") 205 | .build() 206 | 207 | // when 208 | extractManifestAttributes(project).put("Built-Host", "host-1024") 209 | 210 | // then 211 | val manifest = extractManifestMap(project) 212 | assertThat(manifest["Built-Host"]) 213 | .isEqualTo("host-1024") 214 | } 215 | 216 | @Test 217 | fun `should use latest defined manifest attributes`() { 218 | // given 219 | val project = projectWithPlugins("sample-project") 220 | .withGroup("com.coditory") 221 | .withVersion("0.0.1-SNAPSHOT") 222 | .withMainClass("com.coditory.MainClass") 223 | .build() 224 | 225 | // when 226 | project.version = "1.0.0-new-version" 227 | project.group = "new.group.name" 228 | project.extensions.getByType(JavaApplication::class.java).mainClass.set("com.acme.NewClassName") 229 | 230 | // then 231 | val manifest = extractManifestMap(project) 232 | assertThat(manifest["Main-Class"]).isEqualTo("com.acme.NewClassName") 233 | assertThat(manifest["Implementation-Group"]).isEqualTo("new.group.name") 234 | assertThat(manifest["Implementation-Version"]).isEqualTo("1.0.0-new-version") 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/test/kotlin/com/coditory/gradle/manifest/base/GradleTestVersions.kt: -------------------------------------------------------------------------------- 1 | package com.coditory.gradle.manifest.base 2 | 3 | object GradleTestVersions { 4 | const val GRADLE_MAX_SUPPORTED_VERSION = "current" 5 | const val GRADLE_MIN_SUPPORTED_VERSION = "8.7" 6 | } 7 | -------------------------------------------------------------------------------- /src/test/kotlin/com/coditory/gradle/manifest/base/ManifestExtractor.kt: -------------------------------------------------------------------------------- 1 | package com.coditory.gradle.manifest.base 2 | 3 | import org.gradle.api.Project 4 | import org.gradle.api.java.archives.Attributes 5 | import org.gradle.api.plugins.JavaPlugin 6 | import org.gradle.jvm.tasks.Jar 7 | 8 | object ManifestExtractor { 9 | fun extractManifestMap(project: Project): Map { 10 | return extractManifestAttributes(project) 11 | .mapValues { it.value.toString() } 12 | } 13 | 14 | fun extractManifestAttributes(project: Project): Attributes { 15 | return project.tasks.named(JavaPlugin.JAR_TASK_NAME, Jar::class.java).get() 16 | .manifest.effectiveManifest.attributes 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/kotlin/com/coditory/gradle/manifest/base/ManifestPluginWithStubs.kt: -------------------------------------------------------------------------------- 1 | package com.coditory.gradle.manifest.base 2 | 3 | import com.coditory.gradle.manifest.ManifestParameterizedPlugin 4 | import com.coditory.gradle.manifest.attributes.HostNameResolver 5 | 6 | class ManifestPluginWithStubs : ManifestParameterizedPlugin(clock, hostNameProvider) { 7 | companion object { 8 | val clock: UpdatableFixedClock = UpdatableFixedClock() 9 | val hostNameProvider = object : HostNameResolver { 10 | override fun resolveHostName(): String { 11 | return "localhost" 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/test/kotlin/com/coditory/gradle/manifest/base/SystemOutputCapturer.kt: -------------------------------------------------------------------------------- 1 | package com.coditory.gradle.manifest.base 2 | 3 | import java.io.File 4 | import java.io.FileOutputStream 5 | import java.io.PrintStream 6 | import java.util.UUID 7 | import kotlin.io.path.createTempDirectory 8 | 9 | internal class SystemOutputCapturer private constructor( 10 | private val outFile: File, 11 | private val errFile: File, 12 | private val out: PrintStream, 13 | private val err: PrintStream, 14 | ) : AutoCloseable { 15 | @Synchronized 16 | fun readSystemOut(): String { 17 | return outFile.readText() 18 | } 19 | 20 | @Synchronized 21 | fun readSystemErr(): String { 22 | return errFile.readText() 23 | } 24 | 25 | @Synchronized 26 | fun restoreSystemOutput() { 27 | if (System.out !== out || System.err != err) { 28 | throw IllegalStateException("System output was overridden") 29 | } 30 | System.setOut(initialOut) 31 | System.setErr(initialErr) 32 | out.close() 33 | err.close() 34 | } 35 | 36 | @Synchronized 37 | override fun close() { 38 | restoreSystemOutput() 39 | } 40 | 41 | companion object { 42 | private val initialOut = System.out 43 | private val initialErr = System.err 44 | 45 | @Synchronized 46 | @Suppress("EXPERIMENTAL_API_USAGE_ERROR") 47 | fun captureSystemOutput(): SystemOutputCapturer { 48 | if (System.out !== initialOut || System.err != initialErr) { 49 | throw IllegalStateException("System output was already overridden") 50 | } 51 | val tmpDir = createTempDirectory().toFile() 52 | val outFile = createTmpFile(tmpDir, "out.txt") 53 | val errFile = createTmpFile(tmpDir, "err.txt") 54 | val outStream = PrintStream(FileOutputStream(outFile)) 55 | val errStream = PrintStream(FileOutputStream(errFile)) 56 | System.setOut(outStream) 57 | System.setErr(errStream) 58 | return SystemOutputCapturer( 59 | outFile = outFile, 60 | errFile = errFile, 61 | out = outStream, 62 | err = errStream, 63 | ) 64 | } 65 | 66 | private fun createTmpFile(tmpDir: File, suffix: String): File { 67 | val uuid = UUID.randomUUID().toString() 68 | val outFile = tmpDir.resolve("$uuid-$suffix") 69 | outFile.createNewFile() 70 | return outFile 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/test/kotlin/com/coditory/gradle/manifest/base/SystemProperties.kt: -------------------------------------------------------------------------------- 1 | package com.coditory.gradle.manifest.base 2 | 3 | import java.util.Properties 4 | 5 | object SystemProperties { 6 | fun withSystemProperties(properties: Map, call: () -> T): T { 7 | val backup = toMap(System.getProperties()) 8 | setSystemProperties(properties) 9 | try { 10 | return call() 11 | } finally { 12 | setSystemProperties(backup) 13 | } 14 | } 15 | 16 | fun withoutSystemProperties(call: () -> T): T { 17 | val backup = toMap(System.getProperties()) 18 | System.getProperties().clear() 19 | try { 20 | return call() 21 | } finally { 22 | setSystemProperties(backup) 23 | } 24 | } 25 | 26 | private fun toMap(properties: Properties): Map { 27 | return properties.toMap() 28 | .map { it.key.toString() to it.value.toString() } 29 | .toMap() 30 | } 31 | 32 | private fun setSystemProperties(map: Map) { 33 | map.forEach { 34 | System.getProperties().setProperty(it.key, it.value) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/kotlin/com/coditory/gradle/manifest/base/TestProject.kt: -------------------------------------------------------------------------------- 1 | package com.coditory.gradle.manifest.base 2 | 3 | import com.coditory.gradle.manifest.ManifestPlugin 4 | import com.coditory.gradle.manifest.ManifestPluginExtension 5 | import com.coditory.gradle.manifest.ManifestTask 6 | import org.gradle.api.Project 7 | import org.gradle.testkit.runner.BuildResult 8 | import org.gradle.testkit.runner.GradleRunner 9 | import java.io.File 10 | 11 | class TestProject(private val project: Project) : Project by project { 12 | fun toBuildPath(vararg paths: String): String { 13 | return paths.joinToString(File.pathSeparator) { 14 | "${this.layout.buildDirectory.get()}${File.separator}${it.replace("/", File.separator)}" 15 | } 16 | } 17 | 18 | fun readFileFromBuildDir(path: String): String { 19 | return this.layout.buildDirectory.file(path).get().asFile.readText() 20 | } 21 | 22 | fun runGradle(arguments: List, gradleVersion: String? = null): BuildResult { 23 | return gradleRunner(this, arguments, gradleVersion).build() 24 | } 25 | 26 | fun runGradleAndFail(arguments: List, gradleVersion: String? = null): BuildResult { 27 | return gradleRunner(this, arguments, gradleVersion).buildAndFail() 28 | } 29 | 30 | fun readFile(path: String): String { 31 | return projectDir.resolve(path) 32 | .readText() 33 | } 34 | 35 | fun getManifestTask(): ManifestTask { 36 | return this.tasks 37 | .named(ManifestPlugin.GENERATE_MANIFEST_TASK, ManifestTask::class.java) 38 | .get() 39 | } 40 | 41 | fun generateManifest(configure: (ManifestPluginExtension) -> Unit = {}): String { 42 | this.extensions.configure(ManifestPluginExtension::class.java, configure) 43 | getManifestTask().generateManifest() 44 | return this.readFile("build/resources/main/META-INF/MANIFEST.MF") 45 | } 46 | 47 | // Used by @AutoClose test annotation 48 | fun close() { 49 | this.projectDir.deleteRecursively() 50 | } 51 | 52 | fun clean() { 53 | this.runGradle(listOf("clean")) 54 | } 55 | 56 | private fun gradleRunner(project: Project, args: List, gradleVersion: String? = null): GradleRunner { 57 | val builder = GradleRunner.create() 58 | .withProjectDir(project.projectDir) 59 | .withArguments(args) 60 | .withPluginClasspath() 61 | .forwardOutput() 62 | if (!gradleVersion.isNullOrBlank() && gradleVersion != "current") { 63 | builder.withGradleVersion(gradleVersion) 64 | } 65 | return builder 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/test/kotlin/com/coditory/gradle/manifest/base/TestProjectBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.coditory.gradle.manifest.base 2 | 3 | import com.coditory.gradle.manifest.base.TestRepository.Companion.repository 4 | import org.gradle.api.Plugin 5 | import org.gradle.api.internal.project.DefaultProject 6 | import org.gradle.api.plugins.ApplicationPlugin 7 | import org.gradle.api.plugins.JavaApplication 8 | import org.gradle.api.plugins.JavaPlugin 9 | import org.gradle.testfixtures.ProjectBuilder 10 | import java.io.File 11 | import java.nio.file.Files 12 | import kotlin.io.path.createTempDirectory 13 | import kotlin.reflect.KClass 14 | import kotlin.reflect.full.isSuperclassOf 15 | 16 | class TestProjectBuilder private constructor(projectDir: File, name: String) { 17 | private val project = ProjectBuilder.builder() 18 | .withProjectDir(projectDir) 19 | .withName(name) 20 | .build() as DefaultProject 21 | 22 | fun withGroup(group: String): TestProjectBuilder { 23 | project.group = group 24 | return this 25 | } 26 | 27 | fun withVersion(version: String): TestProjectBuilder { 28 | project.version = version 29 | return this 30 | } 31 | 32 | fun withPlugins(vararg plugins: KClass>): TestProjectBuilder { 33 | plugins 34 | .toList() 35 | .onEach { if (it.isSuperclassOf(ManifestPluginWithStubs::class)) ManifestPluginWithStubs.clock.reset() } 36 | .forEach { project.plugins.apply(it.java) } 37 | return this 38 | } 39 | 40 | fun withGitRepository(): TestProjectBuilder { 41 | repository(project) 42 | .withRemote() 43 | .withCommit() 44 | return this 45 | } 46 | 47 | fun withMainClass(mainClass: String): TestProjectBuilder { 48 | project.extensions.getByType(JavaApplication::class.java).mainClass.set(mainClass) 49 | return this 50 | } 51 | 52 | fun withIdeaProjectFiles(): TestProjectBuilder { 53 | project.rootDir.resolve(".idea").createNewFile() 54 | return this 55 | } 56 | 57 | fun withBuildGradleKts(content: String): TestProjectBuilder { 58 | val buildFile = project.rootDir.resolve("build.gradle.kts") 59 | buildFile.writeText(content.trimIndent().trim()) 60 | return this 61 | } 62 | 63 | fun withBuildGradle(content: String): TestProjectBuilder { 64 | val buildFile = project.rootDir.resolve("build.gradle") 65 | buildFile.writeText(content.trimIndent().trim()) 66 | return this 67 | } 68 | 69 | fun withFile(path: String, content: String): TestProjectBuilder { 70 | val filePath = project.rootDir.resolve(path).toPath() 71 | Files.createDirectories(filePath.parent) 72 | val testFile = Files.createFile(filePath).toFile() 73 | testFile.writeText(content.trimIndent().trim()) 74 | return this 75 | } 76 | 77 | fun build(): TestProject { 78 | project.evaluate() 79 | return TestProject(project) 80 | } 81 | 82 | companion object { 83 | fun createProject(): TestProject { 84 | return projectWithPlugins().build() 85 | } 86 | 87 | fun project(name: String = "sample-project", dir: File = createProjectDir(name)): TestProjectBuilder { 88 | return TestProjectBuilder(dir, name) 89 | } 90 | 91 | fun projectWithPlugins(name: String = "sample-project"): TestProjectBuilder { 92 | return project(name) 93 | .withPlugins(JavaPlugin::class, ManifestPluginWithStubs::class, ApplicationPlugin::class) 94 | } 95 | 96 | @Suppress("EXPERIMENTAL_API_USAGE_ERROR") 97 | private fun createProjectDir(directory: String): File { 98 | val projectParentDir = createTempDirectory().toFile() 99 | val projectDir = projectParentDir.resolve(directory) 100 | projectDir.mkdir() 101 | return projectDir 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/test/kotlin/com/coditory/gradle/manifest/base/TestRepository.kt: -------------------------------------------------------------------------------- 1 | package com.coditory.gradle.manifest.base 2 | 3 | import org.gradle.api.Project 4 | import org.gradle.internal.impldep.org.eclipse.jgit.api.Git 5 | import org.gradle.internal.impldep.org.eclipse.jgit.lib.Constants 6 | import org.gradle.internal.impldep.org.eclipse.jgit.lib.PersonIdent 7 | import org.gradle.internal.impldep.org.eclipse.jgit.revwalk.RevCommit 8 | import org.gradle.internal.impldep.org.eclipse.jgit.transport.RemoteConfig 9 | import org.gradle.internal.impldep.org.eclipse.jgit.transport.URIish 10 | import java.time.ZonedDateTime 11 | import java.util.Date 12 | import java.util.TimeZone 13 | import java.util.UUID 14 | 15 | class TestRepository private constructor( 16 | private val project: Project, 17 | private val git: Git, 18 | ) { 19 | 20 | fun withRemote( 21 | name: String = REMOTE_NAME, 22 | url: String = REMOTE_URL, 23 | ): TestRepository { 24 | val config = git.repository.config 25 | val remoteConfig = RemoteConfig(config, name) 26 | remoteConfig.addURI(URIish(url)) 27 | remoteConfig.update(config) 28 | config.save() 29 | return this 30 | } 31 | 32 | fun withCommit( 33 | message: String = COMMIT_MESSAGE, 34 | authorName: String = AUTHOR_NAME, 35 | authorEmail: String = AUTHOR_EMAIL, 36 | authorTime: ZonedDateTime = AUTHOR_TIME, 37 | ): TestRepository { 38 | val uuid = UUID.randomUUID().toString() 39 | val filePath = project.rootDir.resolve("samples/$uuid") 40 | filePath.parentFile.mkdirs() 41 | filePath.createNewFile() 42 | filePath.writeText(uuid) 43 | 44 | val personId = PersonIdent(authorName, authorEmail, Date.from(authorTime.toInstant()), TimeZone.getTimeZone(authorTime.zone)) 45 | git.add().addFilepattern(filePath.absolutePath).call() 46 | git.commit() 47 | .setSign(false) 48 | .setMessage(message) 49 | .setAuthor(personId) 50 | .call() 51 | return this 52 | } 53 | 54 | fun getLastCommit(): RevCommit { 55 | val id = git.repository.resolve(Constants.HEAD) 56 | return git.repository.parseCommit(id) 57 | } 58 | 59 | companion object { 60 | const val REMOTE_NAME = "origin" 61 | const val REMOTE_URL = "git@github.com:coditory/gradle-manifest-plugin.git" 62 | const val COMMIT_MESSAGE = "Very important commit" 63 | const val AUTHOR_NAME = "John Doe" 64 | const val AUTHOR_EMAIL = "john.doe@acme.com" 65 | val AUTHOR_TIME: ZonedDateTime = ZonedDateTime.parse("2020-03-24T20:46:03.242102+01:00") 66 | 67 | fun repository(project: Project): TestRepository { 68 | val git = Git.init() 69 | .setDirectory(project.rootDir) 70 | .setInitialBranch("main") 71 | .call() 72 | return TestRepository(project, git) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/test/kotlin/com/coditory/gradle/manifest/base/UpdatableFixedClock.kt: -------------------------------------------------------------------------------- 1 | package com.coditory.gradle.manifest.base 2 | 3 | import java.time.Clock 4 | import java.time.Duration 5 | import java.time.Duration.ofDays 6 | import java.time.Duration.ofNanos 7 | import java.time.Instant 8 | import java.time.ZoneId 9 | 10 | class UpdatableFixedClock( 11 | private var fixedTime: Instant = DEFAULT_FIXED_TIME, 12 | private var zoneId: ZoneId = DEFAULT_ZONE_ID, 13 | ) : Clock() { 14 | 15 | override fun getZone(): ZoneId { 16 | return zoneId 17 | } 18 | 19 | override fun withZone(zone: ZoneId): UpdatableFixedClock { 20 | return UpdatableFixedClock(this.fixedTime, zoneId) 21 | } 22 | 23 | override fun instant(): Instant { 24 | return fixedTime 25 | } 26 | 27 | fun futureInstant(duration: Duration = ofDays(1)): Instant { 28 | return this.fixedTime + duration 29 | } 30 | 31 | fun pastInstant(duration: Duration = ofDays(1)): Instant { 32 | return this.fixedTime - duration 33 | } 34 | 35 | fun reset() { 36 | this.fixedTime = DEFAULT_FIXED_TIME 37 | } 38 | 39 | fun tick(duration: Duration = ofNanos(1)) { 40 | this.fixedTime = this.fixedTime + duration 41 | } 42 | 43 | fun setup(instant: Instant) { 44 | this.fixedTime = instant 45 | } 46 | 47 | companion object { 48 | private val DEFAULT_FIXED_TIME = Instant.parse("2015-12-03T10:15:30.123456Z") 49 | private val DEFAULT_ZONE_ID = ZoneId.of("Europe/Warsaw") 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/kotlin/com/coditory/gradle/manifest/base/Versions.kt: -------------------------------------------------------------------------------- 1 | package com.coditory.gradle.manifest.base 2 | 3 | object Versions { 4 | val slugify = "2.4" 5 | val hashids = "1.0.3" 6 | val junit = "5.7.0" 7 | val spring = "3.3.4" 8 | } 9 | --------------------------------------------------------------------------------