[Gradle, JS] Necessary to use approach with traversing all source sets
Because common source sets included into JS compilation, it can be detected as JS associated compilations. So necessary to set attribute about JS compiler attribute only (at least at the beginning) to JS only source sets (source sets which are not included into any non js compilations) [Gradle, JS] Add test with resolving JS variant of MPP library [Gradle, JS] Add additional filtering of JS targets [Gradle, JS] Add test on local resolve ^KT-47163 fixed ^KT-47114 fixed
This commit is contained in:
committed by
TeamCityServer
parent
03f85bea8f
commit
594cc29dd2
+69
@@ -10,11 +10,13 @@ import org.gradle.api.logging.LogLevel
|
|||||||
import org.gradle.api.logging.configuration.WarningMode
|
import org.gradle.api.logging.configuration.WarningMode
|
||||||
import org.gradle.util.GradleVersion
|
import org.gradle.util.GradleVersion
|
||||||
import org.jetbrains.kotlin.gradle.plugin.KotlinJsCompilerType
|
import org.jetbrains.kotlin.gradle.plugin.KotlinJsCompilerType
|
||||||
|
import org.jetbrains.kotlin.gradle.plugin.sources.METADATA_CONFIGURATION_NAME_SUFFIX
|
||||||
import org.jetbrains.kotlin.gradle.targets.js.ir.KLIB_TYPE
|
import org.jetbrains.kotlin.gradle.targets.js.ir.KLIB_TYPE
|
||||||
import org.jetbrains.kotlin.gradle.targets.js.npm.*
|
import org.jetbrains.kotlin.gradle.targets.js.npm.*
|
||||||
import org.jetbrains.kotlin.gradle.tasks.USING_JS_INCREMENTAL_COMPILATION_MESSAGE
|
import org.jetbrains.kotlin.gradle.tasks.USING_JS_INCREMENTAL_COMPILATION_MESSAGE
|
||||||
import org.jetbrains.kotlin.gradle.tasks.USING_JS_IR_BACKEND_MESSAGE
|
import org.jetbrains.kotlin.gradle.tasks.USING_JS_IR_BACKEND_MESSAGE
|
||||||
import org.jetbrains.kotlin.gradle.util.*
|
import org.jetbrains.kotlin.gradle.util.*
|
||||||
|
import org.junit.Assert
|
||||||
import org.junit.Assume.assumeFalse
|
import org.junit.Assume.assumeFalse
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -743,6 +745,73 @@ abstract class AbstractKotlin2JsGradlePluginIT(val irBackend: Boolean) : BaseGra
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testResolveJsProjectDependencyToMetadata() = with(Project("kotlin-js-browser-project")) {
|
||||||
|
setupWorkingDir()
|
||||||
|
gradleBuildScript().modify(::transformBuildScriptWithPluginsDsl)
|
||||||
|
gradleSettingsScript().modify(::transformBuildScriptWithPluginsDsl)
|
||||||
|
gradleProperties().appendText("kotlin.js.compiler=both")
|
||||||
|
|
||||||
|
val compiler = if (irBackend) "IR" else "LEGACY"
|
||||||
|
|
||||||
|
val pathPrefix = "metadataDependency: "
|
||||||
|
|
||||||
|
val appBuild = projectDir.resolve("app/build.gradle.kts")
|
||||||
|
appBuild.modify {
|
||||||
|
it.replace("target {", "js($compiler) {")
|
||||||
|
}
|
||||||
|
appBuild.appendText(
|
||||||
|
"\n" + """
|
||||||
|
kotlin.sourceSets {
|
||||||
|
val main by getting {
|
||||||
|
dependencies {
|
||||||
|
// add these dependencies to check that they are resolved to metadata
|
||||||
|
api(project(":base"))
|
||||||
|
implementation(project(":base"))
|
||||||
|
compileOnly(project(":base"))
|
||||||
|
runtimeOnly(project(":base"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task("printMetadataFiles") {
|
||||||
|
doFirst {
|
||||||
|
listOf("api", "implementation", "compileOnly", "runtimeOnly").forEach { kind ->
|
||||||
|
val configuration = configurations.getByName(kind + "DependenciesMetadata")
|
||||||
|
configuration.files.forEach { println("$pathPrefix" + configuration.name + "->" + it.name) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
val metadataDependencyRegex = "$pathPrefix(.*?)->(.*)".toRegex()
|
||||||
|
|
||||||
|
build(
|
||||||
|
"printMetadataFiles",
|
||||||
|
options = defaultBuildOptions().copy(jsCompilerType = if (irBackend) KotlinJsCompilerType.IR else KotlinJsCompilerType.LEGACY)
|
||||||
|
) {
|
||||||
|
assertSuccessful()
|
||||||
|
|
||||||
|
val suffix = if (irBackend) "ir" else "legacy"
|
||||||
|
val ext = if (irBackend) "klib" else "jar"
|
||||||
|
|
||||||
|
val expectedFileName = "base-$suffix.$ext"
|
||||||
|
|
||||||
|
val paths = metadataDependencyRegex
|
||||||
|
.findAll(output).map { it.groupValues[1] to it.groupValues[2] }
|
||||||
|
.filter { (_, f) -> "base" in f }
|
||||||
|
.toSet()
|
||||||
|
|
||||||
|
Assert.assertEquals(
|
||||||
|
listOf("api", "implementation", "compileOnly", "runtimeOnly").map {
|
||||||
|
"$it$METADATA_CONFIGURATION_NAME_SUFFIX" to expectedFileName
|
||||||
|
}.toSet(),
|
||||||
|
paths
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testNoUnintendedDevDependencies() = with(Project("kotlin-js-browser-project")) {
|
fun testNoUnintendedDevDependencies() = with(Project("kotlin-js-browser-project")) {
|
||||||
setupWorkingDir()
|
setupWorkingDir()
|
||||||
|
|||||||
+70
@@ -905,6 +905,76 @@ class NewMultiplatformIT : BaseGradleIT() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testResolveJsPartOfMppLibDependencyToMetadata() {
|
||||||
|
val libProject = Project("sample-lib", gradleVersion, "new-mpp-lib-and-app")
|
||||||
|
val appProject = Project("sample-app", gradleVersion, "new-mpp-lib-and-app")
|
||||||
|
|
||||||
|
libProject.build(
|
||||||
|
"publish",
|
||||||
|
options = defaultBuildOptions().copy(jsCompilerType = BOTH)
|
||||||
|
) {
|
||||||
|
assertSuccessful()
|
||||||
|
}
|
||||||
|
val localRepo = libProject.projectDir.resolve("repo")
|
||||||
|
val localRepoUri = localRepo.toURI()
|
||||||
|
|
||||||
|
with(appProject) {
|
||||||
|
setupWorkingDir()
|
||||||
|
|
||||||
|
val pathPrefix = "metadataDependency: "
|
||||||
|
|
||||||
|
gradleBuildScript().appendText(
|
||||||
|
"\n" + """
|
||||||
|
repositories { maven { url '$localRepoUri' } }
|
||||||
|
|
||||||
|
kotlin.sourceSets {
|
||||||
|
nodeJsMain {
|
||||||
|
dependencies {
|
||||||
|
// add these dependencies to check that they are resolved to metadata
|
||||||
|
api 'com.example:sample-lib-nodejs:1.0'
|
||||||
|
implementation 'com.example:sample-lib-nodejs:1.0'
|
||||||
|
compileOnly 'com.example:sample-lib-nodejs:1.0'
|
||||||
|
runtimeOnly 'com.example:sample-lib-nodejs:1.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task('printMetadataFiles') {
|
||||||
|
doFirst {
|
||||||
|
['Api', 'Implementation', 'CompileOnly', 'RuntimeOnly'].each { kind ->
|
||||||
|
def configuration = configurations.getByName("nodeJsMain${'$'}kind" + '$METADATA_CONFIGURATION_NAME_SUFFIX')
|
||||||
|
configuration.files.each { println '$pathPrefix' + configuration.name + '->' + it.name }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
val metadataDependencyRegex = "$pathPrefix(.*?)->(.*)".toRegex()
|
||||||
|
|
||||||
|
build(
|
||||||
|
"printMetadataFiles",
|
||||||
|
options = defaultBuildOptions().copy(jsCompilerType = IR)
|
||||||
|
) {
|
||||||
|
assertSuccessful()
|
||||||
|
|
||||||
|
val expectedFileName = "sample-lib-nodejsir-1.0.klib"
|
||||||
|
|
||||||
|
val paths = metadataDependencyRegex
|
||||||
|
.findAll(output).map { it.groupValues[1] to it.groupValues[2] }
|
||||||
|
.filter { (_, f) -> "sample-lib" in f }
|
||||||
|
.toSet()
|
||||||
|
|
||||||
|
Assert.assertEquals(
|
||||||
|
listOf("Api", "Implementation", "CompileOnly", "RuntimeOnly").map {
|
||||||
|
"nodeJsMain$it$METADATA_CONFIGURATION_NAME_SUFFIX" to expectedFileName
|
||||||
|
}.toSet(),
|
||||||
|
paths
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testResolveMppProjectDependencyToMetadata() {
|
fun testResolveMppProjectDependencyToMetadata() {
|
||||||
val libProject = Project("sample-lib", gradleVersion, "new-mpp-lib-and-app")
|
val libProject = Project("sample-lib", gradleVersion, "new-mpp-lib-and-app")
|
||||||
|
|||||||
+3
-1
@@ -46,6 +46,8 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
js('nodeJs')
|
||||||
|
|
||||||
targets {
|
targets {
|
||||||
fromPreset(presets.jvm, 'jvm6') {
|
fromPreset(presets.jvm, 'jvm6') {
|
||||||
attributes.attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 6)
|
attributes.attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 6)
|
||||||
@@ -61,7 +63,7 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fromPreset(presets.js, 'nodeJs')
|
|
||||||
fromPreset(presets.linuxX64, 'linux64')
|
fromPreset(presets.linuxX64, 'linux64')
|
||||||
|
|
||||||
configure([linux64]) {
|
configure([linux64]) {
|
||||||
|
|||||||
+8
-3
@@ -59,7 +59,7 @@ interface KotlinTargetConfigurator<KotlinTargetType : KotlinTarget> {
|
|||||||
fun defineConfigurationsForTarget(target: KotlinTargetType)
|
fun defineConfigurationsForTarget(target: KotlinTargetType)
|
||||||
fun configureArchivesAndComponent(target: KotlinTargetType)
|
fun configureArchivesAndComponent(target: KotlinTargetType)
|
||||||
fun configureBuild(target: KotlinTargetType)
|
fun configureBuild(target: KotlinTargetType)
|
||||||
fun configureSourceSet(target: KotlinTarget)
|
fun configureSourceSet(target: KotlinTargetType)
|
||||||
|
|
||||||
fun configurePlatformSpecificModel(target: KotlinTargetType) = Unit
|
fun configurePlatformSpecificModel(target: KotlinTargetType) = Unit
|
||||||
}
|
}
|
||||||
@@ -105,7 +105,7 @@ abstract class AbstractKotlinTargetConfigurator<KotlinTargetType : KotlinTarget>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun configureSourceSet(target: KotlinTarget) {
|
override fun configureSourceSet(target: KotlinTargetType) {
|
||||||
val project = target.project
|
val project = target.project
|
||||||
|
|
||||||
target.compilations.all { compilation ->
|
target.compilations.all { compilation ->
|
||||||
@@ -254,7 +254,12 @@ abstract class AbstractKotlinTargetConfigurator<KotlinTargetType : KotlinTarget>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addDependsOnTaskInOtherProjects(project: Project, taskProvider: TaskProvider<*>, useDependedOn: Boolean, configurationName: String) {
|
private fun addDependsOnTaskInOtherProjects(
|
||||||
|
project: Project,
|
||||||
|
taskProvider: TaskProvider<*>,
|
||||||
|
useDependedOn: Boolean,
|
||||||
|
configurationName: String
|
||||||
|
) {
|
||||||
val configuration = project.configurations.getByName(configurationName)
|
val configuration = project.configurations.getByName(configurationName)
|
||||||
taskProvider.configure { task ->
|
taskProvider.configure { task ->
|
||||||
task.dependsOn(configuration.getTaskDependencyFromProjectDependency(useDependedOn, taskProvider.name))
|
task.dependsOn(configuration.getTaskDependencyFromProjectDependency(useDependedOn, taskProvider.name))
|
||||||
|
|||||||
+65
-3
@@ -7,11 +7,16 @@ package org.jetbrains.kotlin.gradle.plugin.sources
|
|||||||
|
|
||||||
import org.gradle.api.NamedDomainObjectFactory
|
import org.gradle.api.NamedDomainObjectFactory
|
||||||
import org.gradle.api.Project
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.api.artifacts.Configuration
|
||||||
import org.gradle.api.attributes.Usage
|
import org.gradle.api.attributes.Usage
|
||||||
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
|
import org.jetbrains.kotlin.gradle.dsl.kotlinExtension
|
||||||
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
|
import org.jetbrains.kotlin.gradle.plugin.*
|
||||||
|
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation
|
||||||
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinUsages
|
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinUsages
|
||||||
import org.jetbrains.kotlin.gradle.plugin.usageByName
|
import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.targets
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.KotlinJsCompilerAttribute
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.KotlinJsTarget
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget
|
||||||
import org.jetbrains.kotlin.gradle.targets.metadata.isKotlinGranularMetadataEnabled
|
import org.jetbrains.kotlin.gradle.targets.metadata.isKotlinGranularMetadataEnabled
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@@ -92,10 +97,67 @@ internal class DefaultKotlinSourceSetFactory(
|
|||||||
if (project.isKotlinGranularMetadataEnabled) {
|
if (project.isKotlinGranularMetadataEnabled) {
|
||||||
attributes.attribute(Usage.USAGE_ATTRIBUTE, project.usageByName(KotlinUsages.KOTLIN_METADATA))
|
attributes.attribute(Usage.USAGE_ATTRIBUTE, project.usageByName(KotlinUsages.KOTLIN_METADATA))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
project.whenEvaluated {
|
||||||
|
setJsCompilerIfNecessary(sourceSet, this@apply)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KT-47163
|
||||||
|
// It is necessary to set jsCompilerAttribute to configurations which associated with ONLY js source sets
|
||||||
|
// Otherwise configuration cannot be resolved because ambiguity between IR and Legacy variants inside one module
|
||||||
|
private val notOnlyJsSourceSets = mutableSetOf<KotlinSourceSet>()
|
||||||
|
|
||||||
|
private val jsOnlySourceSetsAttributes = mutableMapOf<KotlinSourceSet, KotlinJsCompilerAttribute>()
|
||||||
|
|
||||||
|
private fun setJsCompilerIfNecessary(sourceSet: KotlinSourceSet, configuration: Configuration) {
|
||||||
|
if (sourceSet in notOnlyJsSourceSets) return
|
||||||
|
|
||||||
|
if (sourceSet in jsOnlySourceSetsAttributes) {
|
||||||
|
configuration.attributes.attribute(
|
||||||
|
KotlinJsCompilerAttribute.jsCompilerAttribute,
|
||||||
|
jsOnlySourceSetsAttributes.getValue(sourceSet)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
project.kotlinExtension.targets
|
||||||
|
.filter { it !is KotlinJsIrTarget && it !is KotlinJsTarget }
|
||||||
|
.forEach { target ->
|
||||||
|
target.compilations.forEach { compilation ->
|
||||||
|
notOnlyJsSourceSets.addAll(compilation.allKotlinSourceSets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceSet in notOnlyJsSourceSets) return
|
||||||
|
|
||||||
|
fun chooseCompilerAttribute(target: KotlinTarget): KotlinJsCompilerAttribute {
|
||||||
|
if (target is KotlinJsIrTarget) {
|
||||||
|
return KotlinJsCompilerAttribute.ir
|
||||||
|
}
|
||||||
|
|
||||||
|
target as KotlinJsTarget
|
||||||
|
return if (target.irTarget != null) KotlinJsCompilerAttribute.ir else KotlinJsCompilerAttribute.legacy
|
||||||
|
}
|
||||||
|
|
||||||
|
project.kotlinExtension.targets
|
||||||
|
.filter { it is KotlinJsTarget || it is KotlinJsIrTarget }
|
||||||
|
.forEach { target ->
|
||||||
|
target.compilations
|
||||||
|
.filterIsInstance<KotlinJsCompilation>()
|
||||||
|
.forEach { compilation ->
|
||||||
|
if (sourceSet in compilation.allKotlinSourceSets) {
|
||||||
|
val compilerAttribute = chooseCompilerAttribute(target)
|
||||||
|
jsOnlySourceSetsAttributes[sourceSet] = compilerAttribute
|
||||||
|
configuration.attributes.attribute(KotlinJsCompilerAttribute.jsCompilerAttribute, compilerAttribute)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun doCreateSourceSet(name: String): DefaultKotlinSourceSet {
|
override fun doCreateSourceSet(name: String): DefaultKotlinSourceSet {
|
||||||
return DefaultKotlinSourceSet(project, name)
|
return DefaultKotlinSourceSet(project, name)
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-2
@@ -6,8 +6,9 @@
|
|||||||
package org.jetbrains.kotlin.gradle.targets.js
|
package org.jetbrains.kotlin.gradle.targets.js
|
||||||
|
|
||||||
import org.gradle.api.Named
|
import org.gradle.api.Named
|
||||||
import org.gradle.api.attributes.Attribute
|
import org.gradle.api.attributes.*
|
||||||
import org.gradle.api.attributes.AttributesSchema
|
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
|
||||||
|
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinUsages
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
// For Gradle attributes
|
// For Gradle attributes
|
||||||
@@ -15,6 +16,8 @@ import java.io.Serializable
|
|||||||
enum class KotlinJsCompilerAttribute : Named, Serializable {
|
enum class KotlinJsCompilerAttribute : Named, Serializable {
|
||||||
legacy,
|
legacy,
|
||||||
ir,
|
ir,
|
||||||
|
|
||||||
|
@Deprecated("This value is not used in Gradle plugin. You don't need to use it. It will be removed in next major.")
|
||||||
both;
|
both;
|
||||||
|
|
||||||
override fun getName(): String =
|
override fun getName(): String =
|
||||||
|
|||||||
+6
-1
@@ -52,7 +52,12 @@ constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal val commonFakeApiElementsConfigurationName: String
|
internal val commonFakeApiElementsConfigurationName: String
|
||||||
get() = disambiguateName("commonFakeApiElements")
|
get() = lowerCamelCaseName(
|
||||||
|
irTarget?.let {
|
||||||
|
this.disambiguationClassifierInPlatform
|
||||||
|
} ?: disambiguationClassifier,
|
||||||
|
"commonFakeApiElements"
|
||||||
|
)
|
||||||
|
|
||||||
val disambiguationClassifierInPlatform: String?
|
val disambiguationClassifierInPlatform: String?
|
||||||
get() = if (irTarget != null) {
|
get() = if (irTarget != null) {
|
||||||
|
|||||||
+1
@@ -119,6 +119,7 @@ open class KotlinJsTargetConfigurator :
|
|||||||
isCanBeConsumed = true
|
isCanBeConsumed = true
|
||||||
attributes.attribute<Usage>(Usage.USAGE_ATTRIBUTE, KotlinUsages.producerApiUsage(target))
|
attributes.attribute<Usage>(Usage.USAGE_ATTRIBUTE, KotlinUsages.producerApiUsage(target))
|
||||||
attributes.attribute(KotlinPlatformType.attribute, KotlinPlatformType.common)
|
attributes.attribute(KotlinPlatformType.attribute, KotlinPlatformType.common)
|
||||||
|
setupAsPublicConfigurationIfSupported(target)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+7
-1
@@ -72,7 +72,13 @@ constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal val commonFakeApiElementsConfigurationName: String
|
internal val commonFakeApiElementsConfigurationName: String
|
||||||
get() = disambiguateName("commonFakeApiElements")
|
get() = lowerCamelCaseName(
|
||||||
|
if (mixedMode)
|
||||||
|
disambiguationClassifierInPlatform
|
||||||
|
else
|
||||||
|
disambiguationClassifier,
|
||||||
|
"commonFakeApiElements"
|
||||||
|
)
|
||||||
|
|
||||||
val disambiguationClassifierInPlatform: String?
|
val disambiguationClassifierInPlatform: String?
|
||||||
get() = if (mixedMode) {
|
get() = if (mixedMode) {
|
||||||
|
|||||||
+1
@@ -143,6 +143,7 @@ open class KotlinJsIrTargetConfigurator() :
|
|||||||
isCanBeConsumed = true
|
isCanBeConsumed = true
|
||||||
attributes.attribute<Usage>(Usage.USAGE_ATTRIBUTE, KotlinUsages.producerApiUsage(target))
|
attributes.attribute<Usage>(Usage.USAGE_ATTRIBUTE, KotlinUsages.producerApiUsage(target))
|
||||||
attributes.attribute(KotlinPlatformType.attribute, KotlinPlatformType.common)
|
attributes.attribute(KotlinPlatformType.attribute, KotlinPlatformType.common)
|
||||||
|
setupAsPublicConfigurationIfSupported(target)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user