├── 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 | [![Build Status](https://travis-ci.org/jk1/TeamCity-dependencies-gradle-plugin.png?branch=master)](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 | --------------------------------------------------------------------------------