├── samples
├── settings.gradle
├── build.gradle.kts
└── build.gradle
├── settings.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── tcdeps-groovy
├── src
│ ├── main
│ │ ├── resources
│ │ │ └── META-INF
│ │ │ │ └── gradle-plugins
│ │ │ │ └── com.github.jk1.tcdeps.properties
│ │ └── groovy
│ │ │ └── com.github.jk1.tcdeps
│ │ │ ├── processing
│ │ │ ├── DependencyProcessor.groovy
│ │ │ ├── ModuleVersionResolver.groovy
│ │ │ ├── DependencyPinner.groovy
│ │ │ └── ArtifactRegexResolver.groovy
│ │ │ ├── repository
│ │ │ ├── PinConfiguration.groovy
│ │ │ └── TeamCityRepositoryFactory.groovy
│ │ │ ├── client
│ │ │ ├── Authentication.groovy
│ │ │ ├── RestRequest.groovy
│ │ │ ├── RequestBuilder.groovy
│ │ │ └── RestClient.groovy
│ │ │ ├── util
│ │ │ ├── LogFacade.groovy
│ │ │ ├── PropertyFileCache.groovy
│ │ │ └── ResourceLocator.groovy
│ │ │ ├── model
│ │ │ ├── ArtifactDescriptor.groovy
│ │ │ ├── BuildLocator.groovy
│ │ │ ├── ArtifactVersion.groovy
│ │ │ └── DependencyDescriptor.groovy
│ │ │ └── TeamCityDependenciesPlugin.groovy
│ └── test
│ │ ├── resources
│ │ └── testRepo
│ │ │ └── guestAuth
│ │ │ └── repository
│ │ │ └── download
│ │ │ └── sampleId
│ │ │ └── 1234
│ │ │ └── teamcity-ivy.xml
│ │ └── groovy
│ │ └── com
│ │ └── github
│ │ └── jk1
│ │ └── tcdeps
│ │ ├── MockProject.groovy
│ │ ├── PluginSpec.groovy
│ │ ├── processing
│ │ ├── ModuleVersionResolverSpec.groovy
│ │ └── DependenciesRegexProcessorSpec.groovy
│ │ ├── util
│ │ └── PropertyFileCacheSpec.groovy
│ │ ├── client
│ │ ├── RestResponseSpec.groovy
│ │ ├── RequestBuilderSpec.groovy
│ │ └── RestClientSpec.groovy
│ │ ├── model
│ │ ├── ArtifactVersionSpec.groovy
│ │ ├── ArtifactDescriptorSpec.groovy
│ │ ├── DependencyDescriptorSpec.groovy
│ │ └── BuildLocatorSpec.groovy
│ │ └── TeamCityRepoSpec.groovy
└── build.gradle
├── .travis.yml
├── tcdeps-kt
├── build.gradle
└── src
│ └── main
│ └── kotlin
│ └── com
│ └── github
│ └── jk1
│ └── tcdeps
│ └── KotlinScriptDslAdapter.kt
├── .gitignore
├── gradlew.bat
├── README.md
├── gradlew
└── LICENSE
/samples/settings.gradle:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'tcdeps'
2 |
3 | include 'tcdeps-groovy', 'tcdeps-kt'
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jk1/TeamCity-dependencies-gradle-plugin/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/tcdeps-groovy/src/main/resources/META-INF/gradle-plugins/com.github.jk1.tcdeps.properties:
--------------------------------------------------------------------------------
1 | implementation-class=com.github.jk1.tcdeps.TeamCityDependenciesPlugin
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Jun 02 01:28:06 MSK 2015
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: groovy
2 | jdk: openjdk8
3 | before_cache:
4 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
5 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/
6 | cache:
7 | directories:
8 | - $HOME/.gradle/caches/
9 | - $HOME/.gradle/wrapper/
10 | install: true
--------------------------------------------------------------------------------
/tcdeps-groovy/src/test/resources/testRepo/guestAuth/repository/download/sampleId/1234/teamcity-ivy.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/tcdeps-groovy/src/test/groovy/com/github/jk1/tcdeps/MockProject.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps
2 |
3 | import com.github.jk1.tcdeps.util.ResourceLocator
4 | import org.gradle.testfixtures.ProjectBuilder
5 |
6 | trait MockProject {
7 |
8 | def setup(){
9 | ResourceLocator.setContext(ProjectBuilder.builder().build())
10 | }
11 |
12 | def cleanup(){
13 | ResourceLocator.closeResourceLocator()
14 | }
15 | }
--------------------------------------------------------------------------------
/tcdeps-groovy/src/main/groovy/com.github.jk1.tcdeps/processing/DependencyProcessor.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps.processing
2 |
3 | import com.github.jk1.tcdeps.model.DependencyDescriptor
4 |
5 | trait DependencyProcessor {
6 |
7 | List dependencies = new ArrayList()
8 |
9 | void addDependency(DependencyDescriptor dependency) {
10 | dependencies.add(dependency)
11 | }
12 |
13 | void process() {}
14 | }
--------------------------------------------------------------------------------
/tcdeps-groovy/src/main/groovy/com.github.jk1.tcdeps/repository/PinConfiguration.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps.repository
2 |
3 | class PinConfiguration {
4 | String url
5 | boolean stopBuildOnFail
6 | boolean pinEnabled
7 | String message
8 | String tag
9 | String[] excludes = []
10 |
11 | def setDefaultMessage(String message) {
12 | if (this.message == null) {
13 | this.message = message
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tcdeps-groovy/src/main/groovy/com.github.jk1.tcdeps/client/Authentication.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps.client
2 |
3 | import groovy.transform.Canonical
4 |
5 | @Canonical
6 | class Authentication {
7 | String login
8 | String password
9 |
10 | boolean isRequired() {
11 | login && password
12 | }
13 |
14 | String asHttpHeader(){
15 | String encoded = "$login:$password".bytes.encodeBase64().toString()
16 | return "Basic $encoded"
17 | }
18 | }
--------------------------------------------------------------------------------
/tcdeps-kt/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'org.jetbrains.kotlin.jvm' version '2.2.20'
3 | }
4 |
5 | java.sourceCompatibility = 11
6 | java.targetCompatibility = 11
7 |
8 | dependencies {
9 | implementation project(":tcdeps-groovy")
10 |
11 | implementation gradleApi()
12 | implementation localGroovy()
13 |
14 | implementation 'org.jetbrains.kotlin:kotlin-stdlib:2.2.20'
15 | }
16 |
17 | compileKotlin {
18 | kotlinOptions {
19 | jvmTarget = '11'
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.gitignore.io
2 |
3 | ### Gradle ###
4 | .gradle
5 | build/
6 |
7 | ### Java ###
8 | *.class
9 | *.jar
10 | *.war
11 | *.ear
12 | !*/**/gradle-wrapper.jar
13 |
14 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
15 | hs_err_pid*
16 |
17 | ### Intellij ###
18 | /**/*.iml
19 | .idea/
20 | *.ipr
21 | *.iws
22 | out/
23 | .idea_modules/
24 |
25 | pintest
26 |
27 | ### Eclipse ###
28 | .project
29 | .classpath
30 | .settings/
31 | /bin/
--------------------------------------------------------------------------------
/tcdeps-groovy/src/test/groovy/com/github/jk1/tcdeps/PluginSpec.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps
2 |
3 | import org.gradle.api.Project
4 | import org.gradle.testfixtures.ProjectBuilder
5 | import spock.lang.Specification
6 |
7 | class PluginSpec extends Specification {
8 |
9 | def "plugin should be applicable to a project"(){
10 | Project project = ProjectBuilder.builder().build()
11 |
12 | when:
13 | project.pluginManager.apply 'com.github.jk1.tcdeps'
14 |
15 | then:
16 | project.repositories.teamcityServer
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/tcdeps-groovy/src/main/groovy/com.github.jk1.tcdeps/util/LogFacade.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps.util
2 |
3 |
4 | class LogFacade {
5 |
6 | private static final PREFIX = '[TCdeps]'
7 |
8 | def debug(message) {
9 | ResourceLocator.project.logger.debug("$PREFIX $message")
10 | }
11 |
12 | def info(message) {
13 | ResourceLocator.project.logger.info("$PREFIX $message")
14 | }
15 |
16 | def warn(message) {
17 | ResourceLocator.project.logger.warn("$PREFIX $message")
18 | }
19 |
20 | def warn(message, exception) {
21 | ResourceLocator.project.logger.warn("$PREFIX $message", exception)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/tcdeps-groovy/build.gradle:
--------------------------------------------------------------------------------
1 | plugins{
2 | id 'groovy'
3 | }
4 |
5 | java.sourceCompatibility = 11
6 | java.targetCompatibility = 11
7 |
8 | dependencies {
9 | implementation gradleApi()
10 | implementation localGroovy()
11 |
12 | testImplementation("org.spockframework:spock-core:2.2-groovy-4.0") {
13 | exclude group: 'org.codehaus.groovy'
14 | }
15 | testImplementation gradleTestKit()
16 | testImplementation("org.junit.vintage:junit-vintage-engine:5.14.0")
17 | testImplementation("org.junit.platform:junit-platform-launcher:1.14.0")
18 | }
19 |
20 | test {
21 | useJUnitPlatform()
22 | jvmArgs = ["--add-opens=java.base/java.lang=ALL-UNNAMED"]
23 | }
24 |
--------------------------------------------------------------------------------
/tcdeps-groovy/src/test/groovy/com/github/jk1/tcdeps/processing/ModuleVersionResolverSpec.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps.processing
2 |
3 | import com.github.jk1.tcdeps.model.DependencyDescriptor
4 | import spock.lang.Specification
5 |
6 | class ModuleVersionResolverSpec extends Specification {
7 |
8 | def "Module version resolver should ignore non-changing versions"(){
9 | ModuleVersionResolver resolver = new ModuleVersionResolver()
10 | DependencyDescriptor dependency = DependencyDescriptor.create('bt1:1.0.0:lib.zip')
11 |
12 | when:
13 | resolver.addDependency(dependency)
14 |
15 | then:
16 | dependency.version.version == '1.0.0'
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/tcdeps-groovy/src/main/groovy/com.github.jk1.tcdeps/client/RestRequest.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps.client
2 |
3 | import com.github.jk1.tcdeps.model.BuildLocator
4 | import groovy.transform.Canonical
5 |
6 |
7 | @Canonical
8 | class RestRequest {
9 |
10 | String baseUrl
11 | Closure uriPath
12 | BuildLocator locator
13 |
14 | Authentication authentication = new Authentication()
15 | String body
16 |
17 | String toString(){
18 | if (!baseUrl || !uriPath || !locator){
19 | throw new IllegalArgumentException("Base url, path and locator should be specified")
20 | }
21 | "$baseUrl${uriPath.call(locator, authentication.isRequired())}"
22 | }
23 | }
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/tcdeps-groovy/src/test/groovy/com/github/jk1/tcdeps/util/PropertyFileCacheSpec.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps.util
2 |
3 | import spock.lang.Specification
4 |
5 | class PropertyFileCacheSpec extends Specification {
6 |
7 | private File temp = new File(System.getProperty("java.io.tmpdir"), 'gradlePluginTestCache')
8 |
9 | def cleanup() { temp.deleteDir() }
10 |
11 | def "cache should be able to persist properties"() {
12 | //gradle.getGradleUserHomeDir() >> temp todo!
13 |
14 | when:
15 | def cache = new PropertyFileCache()
16 | cache.store('key', 'value')
17 | cache.flush()
18 |
19 | then:
20 | def otherCache = new PropertyFileCache()
21 | otherCache.load('key').equals('value')
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/samples/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.github.jk1.tcdeps.KotlinScriptDslAdapter.teamcityServer
2 | import com.github.jk1.tcdeps.KotlinScriptDslAdapter.pin
3 | import com.github.jk1.tcdeps.KotlinScriptDslAdapter.tc
4 |
5 | plugins {
6 | id("java")
7 | id("com.github.jk1.tcdeps") version "1.3"
8 | }
9 |
10 | repositories {
11 | teamcityServer {
12 | setUrl("https://teamcity.jetbrains.com")
13 | credentials {
14 | username = "guest"
15 | password = "guest"
16 | }
17 | }
18 | }
19 |
20 | dependencies {
21 | implementation(tc("bt345:1.0.0-beta-3594:kotlin-compiler-1.0.0-beta-3594.zip"))
22 | }
23 |
24 | tasks {
25 | register("listDeps", Task::class) {
26 | doLast {
27 | configurations["compileClasspath"].forEach { it ->
28 | println(it.toString())
29 | }
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/tcdeps-groovy/src/main/groovy/com.github.jk1.tcdeps/model/ArtifactDescriptor.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps.model
2 |
3 | import org.gradle.api.InvalidUserDataException
4 |
5 | class ArtifactDescriptor {
6 |
7 | final String rawPath
8 | final String path
9 | final String name
10 | final String extension
11 |
12 | ArtifactDescriptor(String rawPath) {
13 | if (rawPath == null || rawPath.isEmpty()){
14 | throw new InvalidUserDataException("Artifact path may not be empty, please set at least a filename as artifact path")
15 | }
16 | this.rawPath = rawPath
17 | def lastDotIndex = rawPath.lastIndexOf('.')
18 | name = rawPath[0..lastDotIndex - 1]
19 | extension = rawPath[lastDotIndex + 1..rawPath.size() - 1]
20 | }
21 |
22 | boolean hasPath() {
23 | return path != null
24 | }
25 |
26 | @Override
27 | String toString() {
28 | "Artifact:[rawPath=$rawPath, name=$name, extension=$extension, path=$path]"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tcdeps-groovy/src/test/groovy/com/github/jk1/tcdeps/client/RestResponseSpec.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps.client
2 |
3 | import spock.lang.Specification
4 |
5 | class RestResponseSpec extends Specification {
6 |
7 | def "rest response should be able to store response code and it's body"() {
8 | def response = new RestClient.Response()
9 |
10 | when:
11 | response.code = 200
12 | response.body = "Response body"
13 |
14 | then:
15 | response.code == 200
16 | response.body == "Response body"
17 | }
18 |
19 | def "with no explicit result set rest response assumes transport error"() {
20 | def response = new RestClient.Response()
21 |
22 | expect:
23 | response.code == -1
24 | !response.isOk()
25 | }
26 |
27 | def "2XX http response codes stands for successful result"() {
28 | def response = new RestClient.Response()
29 |
30 | when:
31 | response.code = code
32 |
33 | then:
34 | response.isOk() == result
35 |
36 | where:
37 | code | result
38 | 200 | true
39 | 204 | true
40 | 302 | false
41 | 404 | false
42 | 500 | false
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tcdeps-groovy/src/test/groovy/com/github/jk1/tcdeps/model/ArtifactVersionSpec.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps.model
2 |
3 | import spock.lang.Specification
4 |
5 | class ArtifactVersionSpec extends Specification {
6 |
7 | def "version parser should result in correct build locator"() {
8 | when:
9 | ArtifactVersion version = new ArtifactVersion(versionValue)
10 |
11 | then:
12 | version.buildLocator.equals(locator)
13 |
14 | where:
15 | versionValue | locator
16 | 'lastFinished' | new BuildLocator()
17 | 'TagName.tcbuildtag' | new BuildLocator(tag: 'TagName')
18 | 'lastSuccessful' | new BuildLocator(successful: true)
19 | 'lastPinned' | new BuildLocator(pinned: true)
20 | }
21 |
22 | def "changing module version should require explicit resolution"() {
23 | when:
24 | ArtifactVersion version = new ArtifactVersion(versionValue)
25 |
26 | then:
27 | version.needsResolution == changing
28 | version.changing == changing
29 |
30 | where:
31 | versionValue | changing
32 | 'lastFinished' | true
33 | 'TagName.tcbuildtag' | true
34 | 'lastSuccessful' | true
35 | 'lastPinned' | true
36 | '1.0.0' | false
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/tcdeps-groovy/src/main/groovy/com.github.jk1.tcdeps/util/PropertyFileCache.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps.util
2 |
3 | import org.gradle.api.GradleException
4 | import org.gradle.wrapper.GradleUserHomeLookup
5 |
6 | class PropertyFileCache {
7 |
8 | private File file
9 | private Properties props = new Properties()
10 |
11 | PropertyFileCache() {
12 | file = new File(GradleUserHomeLookup.gradleUserHome(), "caches/tcdeps-resolution.cache")
13 | try {
14 | if (!file.exists()) {
15 | file.parentFile.mkdirs()
16 | file.createNewFile()
17 | }
18 | new FileInputStream(file).withStream {
19 | props.load(it)
20 | }
21 | } catch (IOException e) {
22 | throw new GradleException("TCDeps plugin failed to read cache file", e)
23 | }
24 | }
25 |
26 | void store(String key, String value) {
27 | props.setProperty(key, value)
28 | }
29 |
30 | String load(String key) {
31 | props.getProperty(key)
32 | }
33 |
34 | String flush() {
35 | try {
36 | new FileOutputStream(file).withStream {
37 | props.store(it, 'TeamCity dependencies Gradle plugin cache file. https://github.com/jk1/TeamCity-dependencies-gradle-plugin')
38 | }
39 | } catch (IOException e) {
40 | throw new GradleException("TCDeps plugin failed to flush cached properties", e)
41 | }
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/tcdeps-groovy/src/test/groovy/com/github/jk1/tcdeps/model/ArtifactDescriptorSpec.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps.model
2 |
3 | import org.gradle.api.InvalidUserDataException
4 | import spock.lang.Specification
5 |
6 |
7 | class ArtifactDescriptorSpec extends Specification {
8 |
9 | def "bare files should be parsed as name + extension"() {
10 | ArtifactDescriptor descriptor = new ArtifactDescriptor(raw)
11 |
12 | expect:
13 | descriptor.name.equals(name)
14 | descriptor.extension.equals(ext)
15 | !descriptor.hasPath()
16 | descriptor.path == null
17 |
18 | where:
19 | raw | name | ext
20 | "file.jar" | "file" | "jar"
21 | ".file.zip" | ".file" | "zip"
22 | }
23 |
24 | def "artifact path should be parsed correctly, if exists"() {
25 | ArtifactDescriptor descriptor = new ArtifactDescriptor(raw)
26 |
27 | expect:
28 | descriptor.name.equals(name)
29 | descriptor.extension.equals(ext)
30 |
31 | where:
32 | raw | name | ext
33 | "folder/file.ext" | "folder/file" | "ext"
34 | "dir/archive!/dir/file.ext" | "dir/archive!/dir/file" | "ext"
35 | }
36 |
37 |
38 | def "illegal values should fail the build"() {
39 | when:
40 | new ArtifactDescriptor(path)
41 |
42 | then:
43 | thrown(InvalidUserDataException)
44 |
45 | where:
46 | path << ["", null]
47 | }
48 |
49 |
50 | }
--------------------------------------------------------------------------------
/tcdeps-groovy/src/main/groovy/com.github.jk1.tcdeps/model/BuildLocator.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps.model
2 |
3 | import groovy.transform.Canonical
4 |
5 | /**
6 | * https://confluence.jetbrains.com/display/TCD8/REST+API#RESTAPI-BuildLocator
7 | */
8 | @Canonical
9 | class BuildLocator {
10 |
11 | String buildTypeId
12 | Boolean pinned
13 | Boolean successful
14 | String branch
15 | String tag
16 | String number
17 | String id
18 | Boolean noFilter
19 |
20 | @Override
21 | String toString() {
22 | if (!buildTypeId){
23 | throw new IllegalArgumentException("Build type id is required")
24 | }
25 | def builder = new StringBuilder("buildType:${encode(buildTypeId)}")
26 | if (branch) {
27 | builder.append(",branch:${encode(branch)}")
28 | }
29 | if (tag) {
30 | builder.append(",tags:${encode(tag)}")
31 | }
32 | if (pinned) {
33 | builder.append(",pinned:true")
34 | }
35 | if (successful) {
36 | builder.append(",status:SUCCESS")
37 | }
38 | if (number) {
39 | builder.append(",number:${encode(number)}")
40 | }
41 | if (id) {
42 | builder.append(",id:${encode(id)}")
43 | }
44 | if (noFilter){
45 | builder.append(",defaultFilter:false")
46 | }
47 | return builder.toString()
48 | }
49 |
50 | private def encode(String value) {
51 | return URLEncoder.encode(value, "utf-8")
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/tcdeps-groovy/src/main/groovy/com.github.jk1.tcdeps/model/ArtifactVersion.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps.model
2 |
3 | import org.gradle.api.InvalidUserDataException
4 |
5 | class ArtifactVersion {
6 |
7 | String version
8 | BuildLocator buildLocator
9 | boolean needsResolution = false
10 | boolean changing = false
11 |
12 | // todo: shouldn't it a be full featured parser instead?
13 | private def placeholders = ['lastFinished' : { return new BuildLocator() },
14 | 'sameChainOrLastFinished': { return new BuildLocator() },
15 | 'lastPinned' : { return new BuildLocator(pinned: true) },
16 | 'lastSuccessful' : { return new BuildLocator(successful: true) }]
17 |
18 | public ArtifactVersion(String version) {
19 | if (version == null || version.isEmpty()) {
20 | throw new InvalidUserDataException("version should not be empty")
21 | }
22 | this.version = version
23 | if (placeholders.containsKey(version)) {
24 | needsResolution = true
25 | changing = true
26 | buildLocator = placeholders[version]()
27 | } else if (version.endsWith('.tcbuildtag')) {
28 | needsResolution = true
29 | changing = true
30 | buildLocator = new BuildLocator(tag: version - '.tcbuildtag')
31 | } else {
32 | buildLocator = new BuildLocator(number: version)
33 | }
34 | }
35 |
36 | def resolved(version) {
37 | this.needsResolution = false
38 | this.version = version
39 | }
40 |
41 |
42 | @Override
43 | String toString() {
44 | "Version:[version=$version, resolved=${!needsResolution}]"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/tcdeps-groovy/src/test/groovy/com/github/jk1/tcdeps/model/DependencyDescriptorSpec.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps.model
2 |
3 | import org.gradle.api.InvalidUserDataException
4 | import spock.lang.Specification
5 |
6 | class DependencyDescriptorSpec extends Specification {
7 |
8 | def "legal data should result in valid Gradle dependency notation"() {
9 | expect:
10 | DependencyDescriptor descriptor = DependencyDescriptor.create(raw)
11 | descriptor.toDependencyNotation()[0] == notation
12 |
13 | where:
14 | raw | notation
15 | "btid:1.0:file.jar" | [group:'org', name:'btid', version:'1.0']
16 | [buildTypeId: "btid", version: "1.0", artifactPath: "file.jar"] | [group:'org', name:'btid', version:'1.0']
17 | }
18 |
19 | def "illegal values should fail the build"() {
20 | when:
21 | DependencyDescriptor.create(path)
22 |
23 | then:
24 | thrown(InvalidUserDataException)
25 |
26 | where:
27 | path << [null, "", ":", "btid:1.0", "btid::file.jar",
28 | [foo: 'bar'],
29 | [buildTypeId: "btid", version: "1.0"],
30 | [buildTypeId: "btid", artifactId: "aid"]
31 | ]
32 | }
33 |
34 | def "Changing versions should be supported"() {
35 | when:
36 | DependencyDescriptor descriptor = DependencyDescriptor.create(changing)
37 |
38 | then:
39 | descriptor.toDependencyNotation()[0] == notation
40 |
41 | where:
42 | changing | notation
43 | "btid:lastFinished:file.jar" | [ group: 'org', name: 'btid', version: 'lastFinished']
44 | "btid:TagName.tcbuildtag:file.jar" | [ group: 'org', name: 'btid', version: 'TagName.tcbuildtag']
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/samples/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.github.jk1.tcdeps'
2 | apply plugin: 'java'
3 |
4 | version = '1.0'
5 |
6 | buildscript {
7 | // use locally built plugin
8 | // todo: replace with composite builds?
9 | dependencies {
10 | classpath fileTree(dir: '../build/libs', include: '*.jar')
11 | }
12 | }
13 |
14 | repositories {
15 | teamcityServer {
16 | url = 'https://teamcity.jetbrains.com'
17 | }
18 | }
19 |
20 | configurations.all {
21 | resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
22 | }
23 |
24 | dependencies {
25 | // reference arbitrary files as artifacts
26 | implementation tc('OpenSourceProjects_Peergos_Build:2660:peergoslib.nocache.js')
27 | // with self-explanatory map dependency notation
28 | implementation tc(buildTypeId: 'bt345', version: '1.1.50-dev-1577', artifactPath: 'kotlin-compiler-for-maven.jar')
29 | // subfolders are supported
30 | implementation tc('bt345:1.0.0-beta-3594:KotlinJpsPlugin/kotlin-jps-plugin.jar')
31 | // archive traversal is available with '!' symbol
32 | implementation tc('bt345:1.1.50-dev-1577:kotlin-plugin-1.1.50-dev-1577.zip!/Kotlin/kotlinc/build.txt')
33 | // Use TeamCity version aliases to declare snapshot-like dependencies
34 | implementation tc('ttorrent_Main:lastFinished:ttorrent-1.2.jar')
35 | implementation tc('bt131:lastPinned:javadocs/index.html')
36 | implementation tc('bt337:lastSuccessful:odata4j.zip')
37 | // or reference builds by tags with .tcbuildtag suffix
38 | implementation tc('Kotlin_Protobuf:2.6.1.tcbuildtag:internal/repo/org/jetbrains/kotlin/protobuf-lite/2.6.1/protobuf-lite-2.6.1.jar')
39 | // with feature branches supported
40 | implementation tc(buildTypeId: 'bt390', version: 'lastSuccessful', artifactPath: 'updatePlugins.xml', branch: 'master')
41 | // and basic pattern-matching for artifacts
42 | implementation tc('bt131:lastPinned:javadocs/.*.html')
43 | }
44 |
45 | task listDeps {
46 | doLast {
47 | configurations.compileClasspath.each {
48 | dep -> println(dep)
49 | }
50 | }
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/tcdeps-groovy/src/main/groovy/com.github.jk1.tcdeps/util/ResourceLocator.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps.util
2 |
3 | import com.github.jk1.tcdeps.client.RestClient
4 | import com.github.jk1.tcdeps.repository.PinConfiguration
5 | import org.gradle.api.GradleException
6 | import org.gradle.api.Project
7 | import org.gradle.api.artifacts.repositories.ArtifactRepository
8 | import org.gradle.api.credentials.Credentials
9 |
10 | /**
11 | * Tiny IoC implementation for testability's sake
12 | */
13 | class ResourceLocator {
14 |
15 | static ThreadLocal project = new ThreadLocal<>()
16 |
17 | static PropertyFileCache propertyCache = new PropertyFileCache()
18 |
19 | static RestClient restClient = new RestClient()
20 |
21 | static LogFacade logger = new LogFacade()
22 |
23 | static PinConfiguration pinConfiguration = new PinConfiguration()
24 |
25 | static void setContext(Project theProject) {
26 | project.set(theProject)
27 | }
28 |
29 | static void setPin(PinConfiguration pin) {
30 | pinConfiguration = pin
31 | }
32 |
33 | static void closeResourceLocator() {
34 | propertyCache.flush()
35 | // cleanup to avoid memory leaks in daemon mode
36 | project.remove()
37 | pinConfiguration = new PinConfiguration()
38 | }
39 |
40 | static ArtifactRepository getTeamcityRepository(){
41 | def repo = project.get().repositories.findByName("TeamCity")
42 | if (repo == null) {
43 | throw new GradleException("TeamCity repository is not defined for project ${project.get().name}")
44 | } else {
45 | return repo
46 | }
47 | }
48 |
49 | static PinConfiguration getConfig() {
50 | getTeamcityRepository() // sanity check
51 | return pinConfiguration
52 | }
53 |
54 | static Credentials getCredentials() {
55 | def authentication = getTeamcityRepository().configuredAuthentication
56 | if (authentication.isEmpty()) {
57 | return null
58 | } else {
59 | def credentials = authentication.credentials
60 | return credentials.isEmpty() ? null : credentials[0]
61 | }
62 | }
63 |
64 | static Project getProject() {
65 | return project.get()
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/tcdeps-groovy/src/main/groovy/com.github.jk1.tcdeps/client/RequestBuilder.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps.client
2 |
3 | import com.github.jk1.tcdeps.model.BuildLocator
4 |
5 | class RequestBuilder {
6 |
7 | RestRequest request = new RestRequest()
8 |
9 | RequestBuilder(Closure closure) {
10 | closure.delegate = this // Set delegate of closure to this builder
11 | closure.resolveStrategy = Closure.DELEGATE_ONLY // Only use this builder as the closure delegate
12 | closure()
13 | }
14 |
15 | void baseUrl(String base) {
16 | request.baseUrl = base && base.endsWith("/") ? base[0..-2] : base // remove trailing slash, if any
17 | }
18 |
19 | void login(String login) {
20 | request.authentication.login = login
21 | }
22 |
23 | void password(String password) {
24 | request.authentication.password = password
25 | }
26 |
27 | void locator(BuildLocator locator) {
28 | request.locator = locator
29 | }
30 |
31 | void body(String body) {
32 | request.body = body
33 | }
34 |
35 | void action(Closure closure) {
36 | request.uriPath = closure
37 | }
38 |
39 | static def PIN = { BuildLocator locator, Boolean authenticate ->
40 | if (authenticate) {
41 | "/httpAuth/app/rest/builds/$locator/pin"
42 | } else {
43 | "/guestAuth/app/rest/builds/$locator/pin"
44 | }
45 | }
46 |
47 | static def TAG = { BuildLocator locator, Boolean authenticate ->
48 | if (authenticate) {
49 | "/httpAuth/app/rest/builds/$locator/tags"
50 | } else {
51 | "/guestAuth/app/rest/builds/$locator/tags"
52 | }
53 | }
54 |
55 | static def GET_BUILD_NUMBER = { BuildLocator locator, Boolean authenticate ->
56 | if (authenticate) {
57 | "/httpAuth/app/rest/builds/$locator/number"
58 | } else {
59 | "/guestAuth/app/rest/builds/$locator/number"
60 | }
61 | }
62 |
63 | static def GET_BUILD_ID = { BuildLocator locator, Boolean authenticate ->
64 | if (authenticate) {
65 | "/httpAuth/app/rest/builds/$locator/id"
66 | } else {
67 | "/guestAuth/app/rest/builds/$locator/id"
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/tcdeps-groovy/src/test/groovy/com/github/jk1/tcdeps/model/BuildLocatorSpec.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps.model
2 |
3 | import spock.lang.Specification
4 |
5 | /**
6 | * https://confluence.jetbrains.com/display/TCD8/REST+API#RESTAPI-BuildLocator
7 | */
8 | class BuildLocatorSpec extends Specification {
9 |
10 | def "build locator should require at least build type id"() {
11 | def locator = new BuildLocator()
12 |
13 | when:
14 | locator.toString()
15 |
16 | then:
17 | thrown(IllegalArgumentException)
18 | }
19 |
20 | def "build locator should be serializable into valid path component"() {
21 | def locator = new BuildLocator(buildTypeId: btId, pinned: pin, successful: success,
22 | branch: vcsBranch, tag: tcTag, number: buildNumber)
23 |
24 | expect:
25 | locator.toString().equals(result)
26 |
27 | where:
28 | btId | pin | success | vcsBranch | tcTag | buildNumber | result
29 | 'bt1' | false | false | null | null | null | 'buildType:bt1'
30 | 'bt1' | true | false | null | null | null | 'buildType:bt1,pinned:true'
31 | 'bt1' | true | true | null | null | null | 'buildType:bt1,pinned:true,status:SUCCESS'
32 | 'bt1' | false | false | 'master' | null | null | 'buildType:bt1,branch:master'
33 | 'bt1' | false | false | 'master' | 'ok' | null | 'buildType:bt1,branch:master,tags:ok'
34 | 'bt1' | false | false | null | null | '1.0.15' | 'buildType:bt1,number:1.0.15'
35 | 'bt1' | true | true | 'dev' | 'ok' | '1' | 'buildType:bt1,branch:dev,tags:ok,pinned:true,status:SUCCESS,number:1'
36 | }
37 |
38 | def "build locator should apply URL encoding where necessary"() {
39 | def locator = new BuildLocator(buildTypeId: btId, branch: vcsBranch, tag: tcTag, number: buildNumber)
40 |
41 | expect:
42 | locator.toString().equals(result)
43 |
44 | where:
45 | btId | vcsBranch | tcTag | buildNumber | result
46 | 'bt/' | null | null | null | 'buildType:bt%2F'
47 | 'bt1' | 'ref/heads/branch' | null | null | 'buildType:bt1,branch:ref%2Fheads%2Fbranch'
48 | 'bt1' | null | 'omg!wtf?' | null | 'buildType:bt1,tags:omg%21wtf%3F'
49 | 'bt1' | null | null | '3.14/15%27' | 'buildType:bt1,number:3.14%2F15%2527'
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
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 %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="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 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/tcdeps-groovy/src/test/groovy/com/github/jk1/tcdeps/client/RequestBuilderSpec.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps.client
2 |
3 | import com.github.jk1.tcdeps.model.BuildLocator
4 | import spock.lang.Specification
5 |
6 | class RequestBuilderSpec extends Specification {
7 |
8 | def "request builder should produce anonymous pin url by default"() {
9 | def builder = new RequestBuilder({
10 | baseUrl 'http://server.org'
11 | locator new BuildLocator(buildTypeId: 'bt1', number: '1')
12 | action PIN
13 | })
14 |
15 | expect:
16 | builder.request.toString().equals("http://server.org/guestAuth/app/rest/builds/buildType:bt1,number:1/pin")
17 | }
18 |
19 | def "request builder should produce authneticated pin url if login and password are set"() {
20 | def builder = new RequestBuilder({
21 | baseUrl 'http://server.org'
22 | locator new BuildLocator(buildTypeId: 'bt1', number: '1')
23 | action PIN
24 | login "login"
25 | password "password"
26 | })
27 |
28 | expect:
29 | builder.request.toString().equals("http://server.org/httpAuth/app/rest/builds/buildType:bt1,number:1/pin")
30 | }
31 |
32 | def "request builder should produce valid artifact version resolution url"() {
33 | def builder = new RequestBuilder({
34 | baseUrl 'http://server.org'
35 | action GET_BUILD_NUMBER
36 | locator new BuildLocator(buildTypeId: 'bt1')
37 | })
38 |
39 | expect:
40 | builder.request.toString().equals('http://server.org/guestAuth/app/rest/builds/buildType:bt1/number')
41 | }
42 |
43 | def "request builder should support authentication"() {
44 | def builder = new RequestBuilder({
45 | login userLogin
46 | password userPassword
47 | })
48 |
49 | expect:
50 | builder.request.authentication.isRequired().equals(required)
51 |
52 | where:
53 | userLogin | userPassword | required
54 | null | null | false
55 | 'login' | null | false
56 | null | 'password' | false
57 | 'login' | 'password' | true
58 | }
59 |
60 | def "request builder should fail if some request components are missing"() {
61 | def builder = new RequestBuilder({
62 | baseUrl base
63 | locator buildLocator
64 | action path
65 | })
66 |
67 | when:
68 | builder.request.toString()
69 |
70 | then:
71 | thrown(IllegalArgumentException)
72 |
73 | where:
74 | base | path | buildLocator
75 | "http://server.org" | {} | null
76 | "http://server.org" | null | new BuildLocator()
77 | null | {} | new BuildLocator()
78 | }
79 |
80 | }
--------------------------------------------------------------------------------
/tcdeps-groovy/src/main/groovy/com.github.jk1.tcdeps/processing/ModuleVersionResolver.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps.processing
2 |
3 | import com.github.jk1.tcdeps.client.RestClient
4 | import com.github.jk1.tcdeps.model.BuildLocator
5 | import com.github.jk1.tcdeps.model.DependencyDescriptor
6 | import org.gradle.api.GradleException
7 |
8 | import static com.github.jk1.tcdeps.util.ResourceLocator.*
9 |
10 | /**
11 | * Resolves changing module versions, e.g. lastPinned, against TeamCity feature branches.
12 | * It doesn't look like TeamCity's capable of customizing ivy.xml based on branch locator,
13 | * so we're trying to resolve exact build number beforehand to work this around
14 | */
15 | class ModuleVersionResolver implements DependencyProcessor {
16 |
17 | @Override
18 | void addDependency(DependencyDescriptor dependency) {
19 | if (dependency.getVersion().needsResolution) {
20 | BuildLocator buildLocator = dependency.version.buildLocator
21 | buildLocator.buildTypeId = dependency.buildTypeId
22 | buildLocator.branch = dependency.branch
23 | if (project.gradle.startParameter.offline) {
24 | // offline mode - get the latest cached version
25 | dependency.version.resolved(propertyCache.load(buildLocator.toString()))
26 | logger.info("Unable to resolve $dependency in offline mode, falling back to last cached version")
27 | } else {
28 | String resolvedVersion = doResolve(dependency)
29 | propertyCache.store(buildLocator.toString(), resolvedVersion)
30 | dependency.version.resolved(resolvedVersion)
31 | }
32 | }
33 | }
34 |
35 | private String doResolve(DependencyDescriptor dependency) {
36 | BuildLocator buildLocator = dependency.version.buildLocator
37 | buildLocator.buildTypeId = dependency.buildTypeId
38 | buildLocator.branch = dependency.branch
39 | def response = getBuildNumberFromServer(buildLocator)
40 | if (response.isOk()) {
41 | logger.debug("$dependency.version ($buildLocator) has been resolved to a build #${response.body}")
42 | dependency.version.resolved(response.body)
43 | } else {
44 | String message = "Unable to resolve $dependency.version. \nServer response: \n $response"
45 | throw new GradleException(message)
46 | }
47 | }
48 |
49 | private RestClient.Response getBuildNumberFromServer(BuildLocator buildLocator) {
50 | try {
51 | return restClient.get {
52 | baseUrl config.url
53 | locator buildLocator
54 | action GET_BUILD_NUMBER
55 | login credentials?.username
56 | password credentials?.password
57 | }
58 | } catch (Exception e) {
59 | throw new GradleException("Failed to resolve $buildLocator", e)
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/tcdeps-groovy/src/main/groovy/com.github.jk1.tcdeps/model/DependencyDescriptor.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps.model
2 |
3 | import org.gradle.api.InvalidUserDataException
4 |
5 |
6 | class DependencyDescriptor {
7 |
8 | final String buildTypeId
9 | final ArtifactDescriptor artifactDescriptor
10 | final ArtifactVersion version
11 | final String branch
12 |
13 | protected DependencyDescriptor(
14 | String buildTypeId, ArtifactVersion version, ArtifactDescriptor artifactDescriptor, String branch) {
15 | this.buildTypeId = buildTypeId
16 | this.version = version
17 | this.artifactDescriptor = artifactDescriptor
18 | this.branch = branch
19 | }
20 |
21 | static def create(String buildTypeId, String version, String artifactPath) {
22 | if (buildTypeId == null || buildTypeId.isEmpty()) {
23 | throw new InvalidUserDataException("buildTypeId should not be empty")
24 | }
25 | return new DependencyDescriptor(buildTypeId,
26 | new ArtifactVersion(version),
27 | new ArtifactDescriptor(artifactPath), null)
28 | }
29 |
30 | static def create(String dependencyNotation) {
31 | if (!dependencyNotation) {
32 | throw new InvalidUserDataException("Dependency cannot be empty")
33 | }
34 | String[] dependency = dependencyNotation.split(":")
35 | if (dependency.size() < 3) {
36 | throw new InvalidUserDataException(
37 | "Invalid dependency notation format. Usage: 'buildTypeId:version:artifact'"
38 | )
39 | }
40 | return create(dependency[0], dependency[1], dependency[2])
41 | }
42 |
43 | static def create(Map dependency) {
44 | if (dependency == null) {
45 | throw new InvalidUserDataException("Dependency cannot be empty")
46 | }
47 | def btid = dependency["buildTypeId"]
48 | def version = dependency["version"]
49 | def artifactVersion = new ArtifactVersion(version)
50 | if (!btid) {
51 | throw new InvalidUserDataException("buildTypeId should not be empty")
52 | }
53 | new DependencyDescriptor(btid,
54 | artifactVersion,
55 | new ArtifactDescriptor(dependency["artifactPath"]),
56 | dependency["branch"])
57 | }
58 |
59 | def toDependencyNotation() {
60 | return [[group : 'org',
61 | name : buildTypeId,
62 | version: version.version
63 | ],
64 | { ->
65 | artifact {
66 | name = artifactDescriptor.name
67 | type = artifactDescriptor.extension
68 | }
69 | }]
70 | }
71 |
72 | def toDefaultDependencyNotation() {
73 | return [group : 'org',
74 | name : buildTypeId,
75 | version: version.version
76 | ]
77 | }
78 |
79 | @Override
80 | String toString() {
81 | "Dependency:[buildTypeId=$buildTypeId, artifact=$artifactDescriptor, version=$version, branch=$branch]"
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/tcdeps-kt/src/main/kotlin/com/github/jk1/tcdeps/KotlinScriptDslAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps
2 |
3 | import com.github.jk1.tcdeps.model.DependencyDescriptor
4 | import com.github.jk1.tcdeps.repository.PinConfiguration
5 | import com.github.jk1.tcdeps.util.ResourceLocator
6 | import org.gradle.api.GradleException
7 | import org.gradle.api.Project
8 | import org.gradle.api.artifacts.ModuleDependency
9 | import org.gradle.api.artifacts.dsl.DependencyHandler
10 | import org.gradle.api.artifacts.dsl.RepositoryHandler
11 | import org.gradle.api.artifacts.repositories.IvyArtifactRepository
12 | import org.gradle.internal.artifacts.repositories.AuthenticationSupportedInternal
13 |
14 | object KotlinScriptDslAdapter {
15 |
16 | fun RepositoryHandler.teamcityServer(action: IvyArtifactRepository.() -> Unit): IvyArtifactRepository {
17 | val project = ResourceLocator.getProject()
18 | val plugin = project.getOurPlugin()
19 | val repositories = project.repositories
20 | val oldRepo = repositories.findByName("TeamCity") as IvyArtifactRepository?
21 | val repo = plugin.createTeamCityRepository(project)
22 | action(repo)
23 | if (repo.url == null) {
24 | throw GradleException("TeamCity repository url shouldn't be null")
25 | }
26 | ResourceLocator.getConfig().url = repo.url.toString()
27 | if (oldRepo != null) {
28 | project.logger.warn("Project $project already has TeamCity server ${oldRepo.url}, overriding with ${repo.url}")
29 | repositories.remove(oldRepo)
30 | }
31 |
32 | val tcUrl = repo.url.toString()
33 | val normalizeTcUrl = if (tcUrl.endsWith('/')) tcUrl else "$tcUrl/"
34 |
35 | if (repo.credentials.username.orEmpty().isBlank() && repo.credentials.password.orEmpty().isBlank()) {
36 | (repo as AuthenticationSupportedInternal).setConfiguredCredentials(null)
37 | repo.setUrl("${normalizeTcUrl}guestAuth/repository/download")
38 | } else {
39 | repo.setUrl("${normalizeTcUrl}httpAuth/repository/download")
40 | }
41 | repositories.add(repo)
42 | return repo
43 |
44 | }
45 |
46 | fun IvyArtifactRepository.pin(action: PinConfiguration.() -> Unit) {
47 | val pinConfig = ResourceLocator.getConfig()
48 | action(pinConfig)
49 | pinConfig.pinEnabled = true
50 | ResourceLocator.setPin(pinConfig)
51 | }
52 |
53 | fun DependencyHandler.tc(notation: String): Any {
54 | val plugin = ResourceLocator.getProject().getOurPlugin()
55 | val depDescription = DependencyDescriptor.create(notation) as DependencyDescriptor
56 | plugin.addDependency(depDescription)
57 | // todo: remove duplication
58 | val dep = create(depDescription.toDefaultDependencyNotation()) as ModuleDependency
59 | dep.artifact {
60 | it.name = depDescription.artifactDescriptor.name
61 | it.type = depDescription.artifactDescriptor.extension
62 | }
63 | return dep
64 | }
65 |
66 | private fun Project.getOurPlugin() = plugins.getPlugin("com.github.jk1.tcdeps") as TeamCityDependenciesPlugin
67 | }
--------------------------------------------------------------------------------
/tcdeps-groovy/src/main/groovy/com.github.jk1.tcdeps/repository/TeamCityRepositoryFactory.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps.repository
2 |
3 | import org.gradle.api.Action
4 | import org.gradle.api.Project
5 | import org.gradle.api.artifacts.repositories.IvyArtifactRepository
6 | import org.gradle.api.artifacts.repositories.PasswordCredentials
7 | import org.gradle.api.provider.Property
8 |
9 | class TeamCityRepositoryFactory {
10 |
11 | IvyArtifactRepository createTeamCityRepo(Project project) {
12 |
13 | IvyArtifactRepository repo = createDefaultRepo(project)
14 |
15 | PinConfiguration config = new PinConfiguration()
16 | repo.metaClass.pinConfig = config
17 | repo.metaClass.baseTeamCityURL = ""
18 | repo.metaClass.getPin = { -> return pinConfig }
19 | repo.metaClass.pin = { Closure pinConfigClosure ->
20 | pinConfig.pinEnabled = true
21 | pinConfigClosure.setDelegate(pinConfig)
22 | pinConfigClosure.call()
23 | }
24 |
25 | def oldSetUrl = repo.&setUrl
26 | def oldCredentials = repo.&credentials
27 |
28 | repo.metaClass.setUrl = { Object url ->
29 | baseTeamCityURL = url as String
30 | config.url = normalizeUrl(url)
31 | if (getConfiguredCredentials() instanceof Property) {
32 | if (getConfiguredCredentials().isPresent()) {
33 | oldSetUrl(normalizeUrl(url) + "httpAuth/repository/download")
34 | } else {
35 | oldSetUrl(normalizeUrl(url) + "guestAuth/repository/download")
36 | }
37 | } else {
38 | if (getConfiguredCredentials() != null) {
39 | oldSetUrl(normalizeUrl(url) + "httpAuth/repository/download")
40 | } else {
41 | oldSetUrl(normalizeUrl(url) + "guestAuth/repository/download")
42 | }
43 | }
44 | }
45 | // repo.metaClass.getUrl = { -> return repo.getUrl() }
46 |
47 | repo.metaClass.credentials = { Closure action ->
48 | oldCredentials(new CredentialsConfigurationAction(actionClosure: action, project: project))
49 | oldSetUrl(normalizeUrl(baseTeamCityURL) + "httpAuth/repository/download")
50 | }
51 |
52 | return repo
53 | }
54 |
55 | private IvyArtifactRepository createDefaultRepo(Project project) {
56 | return project.repositories.ivy {
57 | name = 'TeamCity'
58 | patternLayout {
59 | artifact '[module]/[revision]/[artifact](.[ext])'
60 | ivy '[module]/[revision]/teamcity-ivy.xml'
61 | }
62 | content {
63 | includeGroup "org"
64 | }
65 | }
66 | }
67 |
68 | private String normalizeUrl(String url) {
69 | return url.endsWith("/") ? url : url + "/"
70 | }
71 |
72 | private class CredentialsConfigurationAction implements Action {
73 |
74 | Project project
75 | Closure actionClosure
76 |
77 | @Override
78 | void execute(PasswordCredentials passwordCredentials) {
79 | project.configure(passwordCredentials, actionClosure)
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/tcdeps-groovy/src/main/groovy/com.github.jk1.tcdeps/client/RestClient.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps.client
2 |
3 | import groovy.transform.Canonical
4 | import static com.github.jk1.tcdeps.util.ResourceLocator.*
5 |
6 | class RestClient {
7 |
8 | Response get(Closure closure) {
9 | return get(new RequestBuilder(closure).request)
10 | }
11 |
12 | Response get(RestRequest resource) {
13 | return execute("GET", resource)
14 | }
15 |
16 | Response put(Closure closure) {
17 | return put(new RequestBuilder(closure).request)
18 | }
19 |
20 | Response put(RestRequest resource) {
21 | return execute("PUT", resource)
22 | }
23 |
24 | Response post(Closure closure) {
25 | return post(new RequestBuilder(closure).request)
26 | }
27 |
28 | Response post(RestRequest resource) {
29 | return execute("POST", resource)
30 | }
31 |
32 | Response delete(Closure closure) {
33 | return delete(new RequestBuilder(closure).request)
34 | }
35 |
36 | Response delete(RestRequest resource) {
37 | return execute("DELETE", resource)
38 | }
39 |
40 | private Response execute(String method, RestRequest resource) {
41 | HttpURLConnection connection = prepareConnection(method, resource.toString(), resource.authentication)
42 | connection.setInstanceFollowRedirects(true)
43 | writeRequest(connection, resource)
44 | def location = connection.getHeaderField("Location")
45 | if (location != null) { // follow the redirect once
46 | connection = prepareConnection(method, location, resource.authentication)
47 | writeRequest(connection, resource)
48 | }
49 | logger.debug("Request: " + method + " " + resource.toString() +", Response: " + connection.getResponseCode())
50 | return new Response(code: connection.getResponseCode(), body: readResponse(connection))
51 | }
52 |
53 | private HttpURLConnection prepareConnection(String method, String url, Authentication auth) {
54 | HttpURLConnection connection = url.toURL().openConnection()
55 | connection.setRequestMethod(method.toUpperCase())
56 | connection.setRequestProperty("Content-Type", "text/plain")
57 | authenticate(connection, auth)
58 | return connection
59 | }
60 |
61 | private void authenticate(HttpURLConnection connection, Authentication auth) {
62 | if (auth.isRequired()) {
63 | connection.setRequestProperty("Authorization", auth.asHttpHeader())
64 | }
65 | }
66 |
67 | private void writeRequest(HttpURLConnection connection, RestRequest resource) {
68 | if (resource.body) {
69 | connection.setDoOutput(true)
70 | connection.outputStream.withWriter { it << resource.body }
71 | }
72 | }
73 |
74 | private String readResponse(HttpURLConnection connection) {
75 | return (connection.getResponseCode() < 400 ?
76 | connection.inputStream :
77 | connection.getErrorStream()).withReader { Reader reader -> reader.text }
78 | }
79 |
80 | @Canonical
81 | static class Response {
82 | int code = -1 // non-http error, e.g. TLS
83 | String body = "No response recorded. Rerun with --stacktrace to see an exception."
84 |
85 | boolean isOk() {
86 | return (200..<300).contains(code)
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/tcdeps-groovy/src/test/groovy/com/github/jk1/tcdeps/client/RestClientSpec.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps.client
2 |
3 | import com.github.jk1.tcdeps.MockProject
4 | import com.github.jk1.tcdeps.model.BuildLocator
5 | import org.gradle.internal.impldep.org.apache.http.HttpStatus
6 | import spock.lang.Specification
7 |
8 | class RestClientSpec extends Specification implements MockProject {
9 |
10 | private static final String kotlin_build_numbers = '^[0-9.\\-]*(beta|dev|rc)*[0-9.\\-]*$'
11 |
12 | def "Rest client should be able to fetch get build numbers for various build locators"() {
13 | def client = new RestClient()
14 | def response
15 |
16 | when:
17 | response = client.get(new RequestBuilder({
18 | baseUrl 'https://teamcity.jetbrains.com/'
19 | locator buildLocator
20 | action GET_BUILD_NUMBER
21 | }).request)
22 |
23 | then:
24 | response.code == responseCode
25 | response.body.matches(kotlin_build_numbers) // we expect a build number
26 |
27 | where:
28 | buildLocator | responseCode
29 | new BuildLocator(buildTypeId: 'bt345') | HttpStatus.SC_OK
30 | new BuildLocator(buildTypeId: 'bt345', successful: true) | HttpStatus.SC_OK
31 | new BuildLocator(buildTypeId: 'bt345', pinned: true) | HttpStatus.SC_OK
32 | new BuildLocator(buildTypeId: 'bt345', tag: 'bootstrap') | HttpStatus.SC_OK
33 | }
34 |
35 | def "Rest client should be able to handle missing builds"() {
36 | def client = new RestClient()
37 | def response
38 |
39 | when:
40 | response = client.get(new RequestBuilder({
41 | baseUrl 'https://teamcity.jetbrains.com/'
42 | locator buildLocator
43 | action GET_BUILD_NUMBER
44 | }).request)
45 |
46 | then:
47 | response.code == responseCode
48 | !response.body.isEmpty()
49 |
50 | where:
51 | buildLocator | responseCode
52 | new BuildLocator(buildTypeId: 'bt1') | HttpStatus.SC_NOT_FOUND
53 | new BuildLocator(buildTypeId: 'bt345', tag: '42') | HttpStatus.SC_NOT_FOUND
54 | }
55 |
56 | def "basic authentication should be supported"() {
57 | def client = new RestClient()
58 | def response
59 |
60 | when:
61 | response = client.get(new RequestBuilder({
62 | baseUrl 'https://teamcity.jetbrains.com/'
63 | locator new BuildLocator(buildTypeId: 'bt345')
64 | action GET_BUILD_NUMBER
65 | login 'guest'
66 | password 'guest'
67 | }).request)
68 |
69 | then:
70 | response.code == HttpStatus.SC_OK
71 | response.body.matches(kotlin_build_numbers) // we expect a build number
72 | }
73 |
74 | def "client should follow http redirects"() {
75 | def client = new RestClient()
76 | def response
77 |
78 | when:
79 | response = client.get(new RequestBuilder({
80 | baseUrl 'http://teamcity.jetbrains.com/' // http -> https redirect
81 | locator new BuildLocator(buildTypeId: 'bt345')
82 | action GET_BUILD_NUMBER
83 | login 'guest'
84 | password 'guest'
85 | }).request)
86 |
87 | then:
88 | response.code == HttpStatus.SC_OK
89 | response.body.matches(kotlin_build_numbers) // we expect a build number
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/tcdeps-groovy/src/main/groovy/com.github.jk1.tcdeps/TeamCityDependenciesPlugin.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps
2 |
3 | import com.github.jk1.tcdeps.model.DependencyDescriptor
4 | import com.github.jk1.tcdeps.processing.ArtifactRegexResolver
5 | import com.github.jk1.tcdeps.processing.DependencyPinner
6 | import com.github.jk1.tcdeps.processing.DependencyProcessor
7 | import com.github.jk1.tcdeps.processing.ModuleVersionResolver
8 | import com.github.jk1.tcdeps.repository.TeamCityRepositoryFactory
9 | import org.gradle.api.GradleException
10 | import org.gradle.api.Plugin
11 | import org.gradle.api.Project
12 | import org.gradle.api.artifacts.repositories.IvyArtifactRepository
13 | import org.gradle.util.GradleVersion
14 |
15 | import static com.github.jk1.tcdeps.util.ResourceLocator.*
16 |
17 | class TeamCityDependenciesPlugin implements Plugin {
18 |
19 | private List processors
20 | private TeamCityRepositoryFactory teamCityRepositoryFactory = new TeamCityRepositoryFactory()
21 |
22 | @Override
23 | void apply(Project theProject) {
24 | assertCompatibleGradleVersion()
25 | setContext(theProject)
26 | processors = [new ModuleVersionResolver(), new DependencyPinner()]
27 | addTeamCityNotationTo theProject
28 | theProject.ext.tc = { Object notation ->
29 | setContext(theProject)
30 | return addDependency(DependencyDescriptor.create(notation))
31 | }
32 | theProject.afterEvaluate {
33 | setContext(theProject)
34 | processors.each { it.process() }
35 | new ArtifactRegexResolver().process()
36 | }
37 | theProject.gradle.buildFinished { closeResourceLocator() }
38 | }
39 |
40 | public IvyArtifactRepository createTeamCityRepository(Project project) {
41 | return teamCityRepositoryFactory.createTeamCityRepo(project)
42 | }
43 |
44 | private void assertCompatibleGradleVersion() {
45 | def current = GradleVersion.current().version.split("\\.")
46 | def major = current[0].toInteger()
47 | def minor = current[1].split("-")[0].toInteger()
48 | if (major < 5 || (major == 5 && minor < 3)) {
49 | throw new GradleException("TeamCity dependencies plugin requires at least Gradle 5.3. ${GradleVersion.current()} detected.")
50 | }
51 | }
52 |
53 | public Object addDependency(DependencyDescriptor descriptor) {
54 | processors.each { it.addDependency(descriptor) }
55 | def notation = descriptor.toDependencyNotation()
56 | logger.debug("Dependency generated: $notation")
57 | return notation
58 | }
59 |
60 | private void addTeamCityNotationTo(Project project) {
61 | def repositories = project.repositories
62 | repositories.ext.teamcityServer = { Closure configureClosure ->
63 | IvyArtifactRepository oldRepo = repositories.findByName("TeamCity")
64 | IvyArtifactRepository repo = createTeamCityRepository(project)
65 | if (configureClosure) {
66 | configureClosure.rehydrate(repo, configureClosure.owner, configureClosure.thisObject)()
67 | }
68 | if (oldRepo) {
69 | project.logger.warn "Project $project already has TeamCity server [${oldRepo.getUrl()}], overriding with [${repo.getUrl()}]"
70 | repositories.remove(oldRepo)
71 | }
72 | repositories.add(repo)
73 | project.ext.pinConfig = repo.pin
74 | setPin(repo.pin)
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/tcdeps-groovy/src/test/groovy/com/github/jk1/tcdeps/TeamCityRepoSpec.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps
2 |
3 |
4 | import org.gradle.api.Project
5 | import org.gradle.api.artifacts.repositories.IvyArtifactRepository
6 | import org.gradle.testfixtures.ProjectBuilder
7 | import spock.lang.Specification
8 |
9 | class TeamCityRepoSpec extends Specification {
10 |
11 | def "plugin should provide 'teamcity' repository notation"() {
12 | Project project = ProjectBuilder.builder().build()
13 | project.pluginManager.apply 'com.github.jk1.tcdeps'
14 |
15 | when:
16 | project.repositories.teamcityServer()
17 |
18 | then:
19 | project.repositories.findByName("TeamCity") instanceof IvyArtifactRepository
20 | !project.repositories.findByName("TeamCity").pin.pinEnabled
21 | }
22 |
23 |
24 | def "teamcity repository should use guest auth urls when no username is available"() {
25 | Project project = ProjectBuilder.builder().build()
26 | project.pluginManager.apply 'com.github.jk1.tcdeps'
27 |
28 | when:
29 | project.repositories.teamcityServer {
30 | url = "http://teamcity"
31 | }
32 |
33 | then:
34 | project.repositories.findByName("TeamCity").getUrl() == new URI("http://teamcity/guestAuth/repository/download")
35 | }
36 |
37 | def "teamcity repository should use http auth when credentials are provided"() {
38 | Project project = ProjectBuilder.builder().build()
39 | project.pluginManager.apply 'com.github.jk1.tcdeps'
40 |
41 | when:
42 | project.repositories.teamcityServer {
43 | url = "http://teamcity"
44 | credentials {
45 | username "name"
46 | password "secret"
47 | }
48 | }
49 |
50 | then:
51 | project.repositories.findByName("TeamCity").getUrl() == new URI("http://teamcity/httpAuth/repository/download")
52 | }
53 |
54 |
55 |
56 | def "teamcity repository should support pin configuration"() {
57 | Project project = ProjectBuilder.builder().build()
58 | project.pluginManager.apply 'com.github.jk1.tcdeps'
59 |
60 | when:
61 | project.repositories.teamcityServer {
62 | url = "http://teamcity"
63 | credentials {
64 | username = "name"
65 | password = "secret"
66 | }
67 | pin {
68 | stopBuildOnFail = true
69 | message = "Pinned for MyCoolProject"
70 | }
71 | }
72 | def repo = project.repositories.findByName("TeamCity")
73 |
74 | then:
75 | repo.getUrl().toString() == "http://teamcity/httpAuth/repository/download"
76 | repo.credentials.username == "name"
77 | repo.credentials.password == "secret"
78 | repo.pin.pinEnabled
79 | repo.pin.message == "Pinned for MyCoolProject"
80 | repo.pin.stopBuildOnFail
81 | }
82 |
83 | def "Should maintain only one TeamCity server"() {
84 | Project project = ProjectBuilder.builder().build()
85 | project.pluginManager.apply 'com.github.jk1.tcdeps'
86 |
87 | when:
88 | project.repositories.teamcityServer {
89 | url "http://teamcity1"
90 | }
91 | project.repositories.teamcityServer {
92 | url "http://teamcity2"
93 | }
94 |
95 | then:
96 | def repos = project.repositories.findAll { it instanceof IvyArtifactRepository }
97 | repos.size() == 1
98 | repos.get(0).getUrl().toString().contains("teamcity2")
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/tcdeps-groovy/src/test/groovy/com/github/jk1/tcdeps/processing/DependenciesRegexProcessorSpec.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps.processing
2 |
3 | import org.gradle.api.Project
4 | import org.gradle.api.artifacts.ModuleDependency
5 | import org.gradle.testfixtures.ProjectBuilder
6 | import spock.lang.Specification
7 |
8 | import static com.github.jk1.tcdeps.util.ResourceLocator.setContext
9 |
10 | class DependenciesRegexProcessorSpec extends Specification {
11 |
12 | def "Regex processor should not touch exactly matching artifacts"(){
13 | Project project = ProjectBuilder.builder().build()
14 | project.pluginManager.apply 'com.github.jk1.tcdeps'
15 | ArtifactRegexResolver processor = new ArtifactRegexResolver()
16 |
17 | project.repositories.teamcityServer {
18 | url = "file:///" + new File("src/test/resources/testRepo").getAbsolutePath()
19 | }
20 |
21 | project.configurations {
22 | testConfig
23 | }
24 | project.dependencies {
25 | testConfig ("org:sampleId:1234") {
26 | artifact {
27 | name = "foobazbar"
28 | type = "jar"
29 | }
30 | }
31 | }
32 |
33 | when:
34 | setContext(project)
35 | processor.process()
36 | // configuration is resolved
37 | def artifacts = project.configurations.testConfig.resolvedConfiguration.resolvedArtifacts
38 |
39 | then:
40 | artifacts.size() == 1
41 | artifacts.iterator().next().name == "foobazbar"
42 | }
43 |
44 | def "Regex processor should not not do anything until configuration resolution"(){
45 | Project project = ProjectBuilder.builder().build()
46 | project.pluginManager.apply 'com.github.jk1.tcdeps'
47 | ArtifactRegexResolver processor = new ArtifactRegexResolver()
48 |
49 | project.repositories.ivy {
50 | url = "file:///" + new File("does/not/exist").getAbsolutePath()
51 | }
52 |
53 | project.configurations {
54 | testConfig
55 | }
56 | project.dependencies {
57 | testConfig ("org:sampleId:1234") {
58 | artifact {
59 | name = "missing-artifact"
60 | type = "jar"
61 | }
62 | }
63 | }
64 |
65 | when:
66 | setContext(project)
67 | processor.process()
68 | // configuration is not resolved
69 | def dependency = project.configurations.testConfig.dependencies.iterator().next() as ModuleDependency
70 |
71 | then:
72 | dependency.artifacts.size() == 1
73 | dependency.artifacts.iterator().next().name == "missing-artifact"
74 | }
75 |
76 |
77 | def "Regex processor should match simple pattern"(){
78 | Project project = ProjectBuilder.builder().build()
79 | project.pluginManager.apply 'com.github.jk1.tcdeps'
80 | ArtifactRegexResolver processor = new ArtifactRegexResolver()
81 |
82 | project.repositories.teamcityServer {
83 | url = "file:///" + new File("src/test/resources/testRepo").getAbsolutePath()
84 | }
85 |
86 | project.configurations {
87 | testConfig
88 | }
89 |
90 | project.dependencies {
91 | testConfig ("org:sampleId:1234") {
92 | artifact {
93 | name = "foo.*bar"
94 | type = "jar"
95 | }
96 | }
97 | }
98 |
99 | when:
100 | setContext(project)
101 | processor.process()
102 | // configuration is resolved
103 | def artifacts = project.configurations.testConfig.resolvedConfiguration.resolvedArtifacts
104 |
105 | then:
106 | artifacts.size() == 2
107 | artifacts.find { it.name == "foobazbar" }
108 | artifacts.find { it.name == "foolimbar" }
109 | }
110 |
111 | def "Regex processor should handle tc(...) notation"(){
112 | Project project = ProjectBuilder.builder().build()
113 | project.pluginManager.apply 'com.github.jk1.tcdeps'
114 | ArtifactRegexResolver processor = new ArtifactRegexResolver()
115 |
116 | project.repositories.teamcityServer {
117 | url = "file:///" + new File("src/test/resources/testRepo").getAbsolutePath()
118 | }
119 |
120 | project.configurations {
121 | testConfig
122 | }
123 |
124 | project.dependencies {
125 | testConfig project.tc("sampleId:1234:foo.*bar.jar")
126 | }
127 |
128 | when:
129 | setContext(project)
130 | processor.process()
131 | // configuration is resolved
132 | def artifacts = project.configurations.testConfig.resolvedConfiguration.resolvedArtifacts
133 |
134 | then:
135 | artifacts.size() == 2
136 | artifacts.find { it.name == "foobazbar" }
137 | artifacts.find { it.name == "foolimbar" }
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/tcdeps-groovy/src/main/groovy/com.github.jk1.tcdeps/processing/DependencyPinner.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps.processing
2 |
3 | import com.github.jk1.tcdeps.client.RestClient
4 | import com.github.jk1.tcdeps.model.BuildLocator
5 | import com.github.jk1.tcdeps.model.DependencyDescriptor
6 | import org.gradle.api.GradleException
7 |
8 | import static com.github.jk1.tcdeps.util.ResourceLocator.*
9 |
10 | /**
11 | * Pin: PUT http://teamcity:8111/httpAuth/app/rest/builds/buildType:%s,number:%s/pin/
12 | * (the text in the request data is added as a comment for the action)
13 | *
14 | */
15 | class DependencyPinner implements DependencyProcessor {
16 |
17 | @Override
18 | void process() {
19 | if (!dependencies.isEmpty()) {
20 | config.setDefaultMessage("Pinned when building dependent build $project.name $project.version")
21 | if (config.pinEnabled) {
22 | dependencies.findAll { shouldPin(it) }.unique().each {
23 | def buildId = resolveBuildId(it)
24 | if (buildId) {
25 | pinBuild(buildId, it)
26 | if (config.tag){
27 | tagBuild(buildId, it)
28 | }
29 | }
30 | }
31 | } else {
32 | logger.debug("Dependency pinning is disabled")
33 | }
34 | }
35 | }
36 |
37 | /**
38 | * Dependencies with dynamic versions and explicit excludes should not be pinned or tagged
39 | */
40 | private def shouldPin(DependencyDescriptor dep){
41 | return !dep.version.changing && !config.excludes.contains(dep.buildTypeId)
42 | }
43 |
44 | private def pinBuild(String buildId, DependencyDescriptor dependency) {
45 | BuildLocator buildLocator = dependency.version.buildLocator
46 | buildLocator.buildTypeId = dependency.buildTypeId
47 | buildLocator.id = buildId
48 | logger.debug("Pinning the build: $buildLocator")
49 | try {
50 | def response = restClient.put {
51 | baseUrl config.url
52 | locator buildLocator
53 | action PIN
54 | body config.message
55 | login credentials?.username
56 | password credentials?.password
57 | }
58 | assertResponse(response, buildLocator)
59 | } catch (Exception e) {
60 | handleException(e, buildLocator)
61 | }
62 | }
63 |
64 | private def tagBuild(String buildId, DependencyDescriptor dependency) {
65 | BuildLocator buildLocator = dependency.version.buildLocator
66 | buildLocator.buildTypeId = dependency.buildTypeId
67 | buildLocator.id = buildId
68 | logger.debug("Tagging the build: $buildLocator")
69 | try {
70 | def response = restClient.post {
71 | baseUrl config.url
72 | locator buildLocator
73 | action TAG
74 | body config.tag
75 | login credentials?.username
76 | password credentials?.password
77 | }
78 | assertResponse(response, buildLocator)
79 | } catch (Exception e) {
80 | handleException(e, buildLocator)
81 | }
82 | }
83 |
84 | private def resolveBuildId(DependencyDescriptor dependency) {
85 | BuildLocator buildLocator = dependency.version.buildLocator
86 | buildLocator.buildTypeId = dependency.buildTypeId
87 | buildLocator.noFilter = true
88 | try {
89 | def response = restClient.get {
90 | baseUrl config.url
91 | locator buildLocator
92 | action GET_BUILD_ID
93 | login credentials?.username
94 | password credentials?.password
95 | }
96 | assertResponse(response, buildLocator)
97 | return response.body
98 | } catch (Exception e) {
99 | handleException(e, buildLocator)
100 | return null
101 | }
102 | }
103 |
104 | private def handleException(Exception e, BuildLocator buildLocator) {
105 | if (e instanceof GradleException) {
106 | throw e
107 | }
108 | String message = "Unable to pin/tag build: $buildLocator"
109 | if (config.stopBuildOnFail) {
110 | throw new GradleException(message, e)
111 | } else {
112 | logger.warn(message, e)
113 | }
114 | }
115 |
116 | private def assertResponse(RestClient.Response response, BuildLocator buildLocator) {
117 | if (response && !response.isOk()) {
118 | String message = "Unable to pin/tag build: $buildLocator. Server response: HTTP $response.code \n $response.body"
119 | if (config.stopBuildOnFail) {
120 | throw new GradleException(message)
121 | } else {
122 | logger.warn(message)
123 | }
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | TeamCity-gradle-plugin
2 | ======================
3 | [](https://travis-ci.org/jk1/TeamCity-dependencies-gradle-plugin)
4 |
5 | Allows the use of [JetBrains TeamCity](http://www.jetbrains.com/teamcity/) server as an external dependency repository for Gradle builds. This comes in handy when existing artifact layout ignores any established conventions, so out-of-box repository types just can't handle it.
6 |
7 | The plugin makes use of default artifact cache, downloading each dependency only once.
8 |
9 | ### Simple example
10 |
11 | ```groovy
12 | plugins {
13 | // Gradle 6
14 | id 'com.github.jk1.tcdeps' version '1.3.2'
15 | }
16 |
17 | plugins {
18 | // Gradle 7-8
19 | id 'com.github.jk1.tcdeps' version '1.6.2'
20 | }
21 |
22 | plugins {
23 | // Gradle 9
24 | id 'com.github.jk1.tcdeps' version '1.7.0'
25 | }
26 |
27 | repositories{
28 | teamcityServer{
29 | url = 'https://teamcity.jetbrains.com'
30 | // credentials section is optional
31 | credentials {
32 | username = "login"
33 | password = "password"
34 | }
35 | }
36 | }
37 |
38 | dependencies {
39 | // reference arbitrary files as artifacts
40 | compile tc('bt345:1.0.0-beta-3594:kotlin-compiler-1.0.0-beta-3594.zip')
41 |
42 | // with self-explanatory map dependency notation
43 | compile tc(buildTypeId: 'bt345', version: '1.0.0-beta-3594', artifactPath: 'kotlin-compiler-for-maven.jar')
44 |
45 | // subfolders are supported
46 | compile tc('bt345:1.0.0-beta-3594:KotlinJpsPlugin/kotlin-jps-plugin.jar')
47 |
48 | // archive traversal is available with '!' symbol
49 | compile tc('bt345:1.0.0-beta-3594:kotlin-compiler-1.0.0-beta-3594.zip!/kotlinc/build.txt')
50 |
51 | // as well as basic pattern-matching for artifacts
52 | compile tc('bt415:lastSuccessful:.*-scala.jar')
53 | }
54 | ```
55 | TeamCity dependency description consist of the following components: build type id, build number aka version, and artifact path. Artifact path should be relative to build artifacts root in TC build.
56 |
57 | ### Kotlin script support
58 |
59 | The following example demonstrates how to use the plugin in [Kotlin scripted builds](https://github.com/gradle/gradle-script-kotlin):
60 |
61 | ```
62 | import com.github.jk1.tcdeps.KotlinScriptDslAdapter.teamcityServer
63 | import com.github.jk1.tcdeps.KotlinScriptDslAdapter.pin
64 | import com.github.jk1.tcdeps.KotlinScriptDslAdapter.tc
65 |
66 | plugins {
67 | java
68 | id("com.github.jk1.tcdeps") version "1.6.2"
69 | }
70 |
71 |
72 | repositories {
73 | teamcityServer {
74 | setUrl("https://teamcity.jetbrains.com")
75 | // credentials section is optional
76 | credentials {
77 | username = "login"
78 | password = "password"
79 | }
80 | }
81 | }
82 |
83 | dependencies {
84 | compile(tc("bt345:1.1.50-dev-1182:kotlin-compiler1.1.50-dev-1182.zip"))
85 | }
86 | ```
87 |
88 | ### Changing dependencies
89 |
90 | Plugin supports TeamCity build version placeholders:
91 |
92 | ```groovy
93 | dependencies {
94 | compile tc('bt351:lastFinished:plugin-verifier.jar')
95 | compile tc('bt131:lastPinned:javadocs/index.html')
96 | compile tc('bt337:lastSuccessful:odata4j.zip')
97 | compile tc('IntelliJIdeaCe_OpenapiJar:sameChainOrLastFinished:idea_rt.jar')
98 | }
99 | ```
100 |
101 | and tags with `.tcbuildtag` version suffix notation:
102 |
103 | ```groovy
104 | dependencies {
105 | // Latest build marked with tag 'hub-1.0'
106 | compile tc('Xodus_Build:hub-1.0.tcbuildtag:console/build/libs/xodus-console.jar')
107 | }
108 | ```
109 |
110 | these dependencies will be resolved every build.
111 |
112 | Changing dependencies may be also resolved against particular [feature branches](https://confluence.jetbrains.com/display/TCD8/Working+with+Feature+Branches):
113 |
114 | ```groovy
115 | dependencies {
116 | compile tc(buildTypeId: 'bt390', version: 'lastSuccessful', artifactPath: 'updatePlugins.xml', branch: 'master')
117 | }
118 | ```
119 |
120 | Branch name should be specified exactly as it's known to TeamCity with no encoding applied.
121 | Default branch will be used if branch value is not specified explicitly.
122 |
123 | ### Pinning the build
124 |
125 | By default, TeamCity does not store artifacts indefinitely, deleting them after some time. To avoid dependency loss one may choose to [pin the build](https://confluence.jetbrains.com/display/TCD8/Pinned+Build) as follows:
126 |
127 | ```groovy
128 | repositories{
129 | teamcityServer{
130 | url = 'https://teamcity.jetbrains.com'
131 | credentials {
132 | username = "login"
133 | password = "password"
134 | }
135 | pin {
136 | stopBuildOnFail = true // not mandatory, default to 'false'
137 | message = "Pinned for MyProject" // optional pin message
138 | tag = "Production" // optional build tag
139 | excludes = ["MyBuildTypeId"] // exclude build type ids from pinning/tagging
140 | }
141 | }
142 | }
143 | ```
144 | "tag" property allows to assign a custom TC tag to a build your project depends on.
145 |
146 | ### Offline mode
147 |
148 | Gradle's offline mode is fully supported for TeamCity-originated dependencies. This feature allows you to run the build when teamcity server is unreacheable or down using artifacts from local Gradle's cache:
149 |
150 | ```
151 | gradle jar --offline
152 | ```
153 |
154 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/tcdeps-groovy/src/main/groovy/com.github.jk1.tcdeps/processing/ArtifactRegexResolver.groovy:
--------------------------------------------------------------------------------
1 | package com.github.jk1.tcdeps.processing
2 |
3 | import com.github.jk1.tcdeps.util.ResourceLocator
4 | import groovy.transform.Canonical
5 | import org.gradle.api.artifacts.Configuration
6 | import org.gradle.api.artifacts.DependencyArtifact
7 | import org.gradle.api.artifacts.ModuleDependency
8 | import org.gradle.api.artifacts.result.ComponentArtifactsResult
9 | import org.gradle.api.artifacts.result.ResolvedArtifactResult
10 | import org.gradle.api.artifacts.result.ResolvedDependencyResult
11 | import org.gradle.ivy.IvyDescriptorArtifact
12 | import org.gradle.ivy.IvyModule
13 | import groovy.xml.XmlSlurper
14 |
15 | import static com.github.jk1.tcdeps.util.ResourceLocator.logger
16 | import static com.github.jk1.tcdeps.util.ResourceLocator.project
17 |
18 | class ArtifactRegexResolver {
19 |
20 | def process() {
21 | try {
22 | // make configuration resolution as lazy, as possible
23 | project.configurations.findAll { it.state != Configuration.State.UNRESOLVED }.each { configuration ->
24 | resolveArtifacts(configuration)
25 | }
26 | def capturedProject = project
27 | project.configurations.findAll { it.state == Configuration.State.UNRESOLVED }.each { configuration ->
28 | configuration.incoming.beforeResolve { incoming ->
29 | // Make sure the closure won't run on configuration copy. See https://github.com/gradle/gradle/pull/1603
30 | if (incoming == configuration.incoming) {
31 | ResourceLocator.setContext(capturedProject)
32 | resolveArtifacts(configuration)
33 | }
34 | }
35 | }
36 | } catch (Throwable e) {
37 | /*
38 | * The code below depends on internal Gradle classes and is therefore quite fragile.
39 | * Artifact wildcard are not used by most of plugin users so we don't want the whole
40 | * build to fail if something got changed in future Gradle versions.
41 | * Just log the error and skip wildcard resolution step instead.
42 | */
43 | logger.warn('An error occurred during artifact notation pattern resolution', e)
44 | }
45 | }
46 |
47 | def resolveArtifacts(Configuration configuration) {
48 | logger.debug("Processing $project, $configuration")
49 |
50 | def ivyDescriptors = getIvyDescriptorsForConfiguration(configuration.copy())
51 |
52 | ivyDescriptors.findAll { hasIvyArtifact(it) }.each { component ->
53 |
54 | def ivyFile = getIvyArtifact(component)
55 | // TODO or should it be multiple dependencies per component?
56 | ModuleDependency targetDependency = findRelatedDependency(component, configuration)
57 |
58 | if (targetDependency != null) {
59 | logger.debug("Dependency [$targetDependency] has ivy file [$ivyFile], parsing")
60 |
61 | def ivyDefinedArtifacts = readArtifactsSet(ivyFile)
62 | Set toAdd = new HashSet<>()
63 | def i = targetDependency.getArtifacts().iterator()
64 | while (i.hasNext()) {
65 | DependencyArtifact da = i.next()
66 | String daName = "${da.name}.${da.type}".toString()
67 | def candidates = []
68 | logger.debug("processing dependency artifact [${daName}]")
69 | def exactEqual = ivyDefinedArtifacts.find {
70 | if (daName == it.toString()) {
71 | return true
72 | } else {
73 | if (it.toString() ==~ $/${daName}/$) {
74 | candidates.add(it)
75 | }
76 | return false
77 | }
78 | }
79 | logger.debug("got exact equal [${exactEqual}] and candidates [${candidates}]")
80 | def hasMatches = candidates.size() > 0
81 | if (exactEqual == null && hasMatches) {
82 | i.remove()
83 | toAdd.addAll(candidates)
84 | }
85 | }
86 |
87 | toAdd.each { IvyArtifactName artifact ->
88 | logger.debug("injecting new artifact [${artifact.toString()}]")
89 | targetDependency.artifact {
90 | name = artifact.name
91 | type = artifact.extension
92 | }
93 | }
94 | }
95 | }
96 | }
97 |
98 | private Set readArtifactsSet(File ivyFile) {
99 | project.logger.debug("Parsing ivy file [$ivyFile]")
100 | def ivyModule = new XmlSlurper().parseText(ivyFile.text)
101 | return ivyModule.publications.childNodes().collect {
102 | new IvyArtifactName(name: it.attributes().get("name"), extension: it.attributes().get("ext"))
103 | }
104 | }
105 |
106 | private ModuleDependency findRelatedDependency(component, configuration) {
107 | (ModuleDependency) configuration.dependencies.find { dep ->
108 | dep instanceof ModuleDependency &&
109 | "$dep.group:$dep.name:$dep.version" == component.id.displayName
110 | }
111 | }
112 |
113 | private boolean hasIvyArtifact(ComponentArtifactsResult component) {
114 | File ivyFile = getIvyArtifact(component)
115 | if (ivyFile == null) {
116 | logger.debug("No ivy descriptor for component [$component.id.displayName].")
117 | false
118 | }
119 | true
120 | }
121 |
122 | private File getIvyArtifact(ComponentArtifactsResult component) {
123 | return component.getArtifacts(IvyDescriptorArtifact).find { it instanceof ResolvedArtifactResult }?.file
124 | }
125 |
126 | private Set getIvyDescriptorsForConfiguration(Configuration configuration) {
127 | def componentIds = configuration.incoming.resolutionResult.allDependencies
128 | .findAll { it instanceof ResolvedDependencyResult }
129 | .collect { it.selected.id }
130 |
131 | if (componentIds.isEmpty()) {
132 | logger.debug("no components found")
133 | return []
134 | } else {
135 | logger.debug("component ids $componentIds")
136 | }
137 | getIvyDescriptorsForComponents(componentIds)
138 | }
139 |
140 | private Set getIvyDescriptorsForComponents(List componentIds) {
141 | project.dependencies.createArtifactResolutionQuery()
142 | .forComponents(componentIds)
143 | .withArtifacts(IvyModule, IvyDescriptorArtifact)
144 | .execute().resolvedComponents
145 | }
146 |
147 | @Canonical
148 | private class IvyArtifactName {
149 |
150 | String name
151 | String extension
152 |
153 | @Override
154 | String toString() {
155 | return extension == null ? name : "$name.$extension"
156 | }
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
203 |
--------------------------------------------------------------------------------