Як і навіщо переходити з Groovy на Kotlin у скриптах збирання

Усі статті, обговорення, новини про Mobile — в одному місці. Підписуйтеся на телеграм-канал!

Усім привіт. Я Сергій Моренець, розробник, викладач, спікер та технічний письменник. Хочу поділитися з вами своїм досвідом міграції коду з Groovy на Kotlin. Я вже описав переваги Kotlin в окремій статті. Популярність Kotlin зростає, Groovy падає, і ті завдання, для вирішення яких раніше застосовувався Groovy, тепер можна написати на Kotlin. У статті йтиметься про міграцію скриптів збірки Gradle. Ну і за останні роки у нас накопичилося достатньо досвіду роботи з таким підходом, і ми розглядаємо ці технології на тренінгах з Kotlin

Мета міграції

Якщо вашому Java-проєкту не більше як 10 років, то під час його створення у вас був вибір серед двох систем збирання — Maven та Gradle. Maven ще з початку 2010-х був найпопулярнішою системою збирання, а Gradle з моменту свого народження у 2011-му — найперспективнішою. Ситуація не надто змінилася з того часу, хоча Gradle практично наздогнав Maven в популярності.

Раніше Gradle підтримував лише Groovy для написання скриптів, але у травні 2016 року (у версії 3.0) оголосив про підтримку Kotlin як другої мови. Ймовірно, це було пов’язано з виходом Kotlin 1.0 — першої стабільної версії. Стало очевидно, що Kotlin — це не чиясь забава, а досить багатонадійна технологія, за якою майбутнє.

Самі розробники Gradle пояснили, що з додаванням підтримки Kotlin програмістам простіше писати скрипти завдяки:

  • автоматичному доповненню до IDE;
  • зручній документація;
  • можливості рефакторингу;
  • навігації за вихідними джерелами.

Працювати з Kotlin зручніше, ніж з Groovy, оскільки це статично типізована мова. Цікаво, що зараз, за 7 років, Gradle все ще підтримує Groovy — можливо, тому що компіляція Groovy-скриптів досі працює швидше. Тим не менш, через це в Gradle-дистрибутив вбудовані й Groovy-бібліотеки (11 МБ), і Kotlin-компілятор (56 МБ) та багато інших Kotlin-бібліотек. А це перетворило дистрибутив Gradle на справжнього 100-мегабайтного монстра. Для порівняння: Maven займає лише 10 МБ.

У будь-якому випадку Kotlin є перспективнішою мовою, ніж Groovy. А все тому, що Kotlin, як і Gradle, використовується для Android-проєктів. Як перейти на Kotlin у скриптах збирання? Це не так просто, і проєкт OpenRewrite, який я нещодавно використовував, нам не допоможе. На жаль, сам Gradle не надає засобів для автоматичної міграції, хоча це не так складно. Потрібно лише завантажити Groovy-скрипт (у внутрішнє уявлення DSL), а потім зберегти його в форматі Kotlin. На жаль, це досі не підтримується, тому такий перехід потрібно робити вручну — для цього необхідно добре знати і Groovy, і Kotlin.

Спробуємо крок за кроком переписати наші скрипти, а заразом і порівняємо розмір скриптів та продуктивність складання.

Початок міграції

Перший природний крок — перейменування скрипта збірки, оскільки Gradle інтерпретує файли з розширенням *.gradle як Groovy-скрипти. Візьмемо перший скрипт і перейменуємо його з build.gradle на build.gradle.kts. На щастя, Gradle дозволяє змішувати в одному проєкті скрипти на Groovy та Kotlin. Спочатку файл мав такий вигляд:

dependencies {
    implementation project(':common')
    implementation("jakarta.validation:jakarta.validation-api:3.0.2")  
    api 'io.swagger.core.v3:swagger-annotations:2.2.8'   
 
    testImplementation("com.github.tomakehurst:wiremock-jre8:2.35.0")
}

Запускаємо збiрку gradle assemble та отримуємо два види помилок:

implementation project(':common')
                     ^ Too many characters in a character literal '':common''

Причина помилки: Kotlin не підтримує апострофи для оголошення рядка (на відміну від Groovy), тому доведеться перейти на лапки:

    implementation project(":common")

Знову збираємо складання і тепер отримуємо іншу помилку:

api "io.swagger.core.v3:swagger-annotations:2.2.8"
     ^ Unexpected tokens (use ';' to separate expressions on the same line)

У Groovy можна вказувати значення аргументів, розділяючи їх прогалиною від назви методу. У Kotlin таке неприпустимо, тому потрібно використовувати звичний спосіб — дужки:

    api("io.swagger.core.v3:swagger-annotations:2.2.8")   

Унаслідок цього скрипт збирання має такий вигляд:

dependencies {
    implementation(project(":common"))
    implementation("jakarta.validation:jakarta.validation-api:3.0.2")  
    api("io.swagger.core.v3:swagger-annotations:2.2.8")   
 
    testImplementation("com.github.tomakehurst:wiremock-jre8:2.35.0")
}

Цікаво, що блок dependencies перебудовувати не довелося, оскільки він у Kotlin DSL не змінився і приймає об’єкт типу Groovy Closure:

abstract fun dependencies(configureClosure: Closure)

А Groovy Closure — це аналог лямбд у Kotlin з ідентичним синтаксисом. Тепер перейдемо до другого скрипту збирання, де трапляється інша помилка:

  api("io.swagger.core.v3:swagger-annotations:$swaggerVersion")
                                              ^ Unresolved reference: swaggerVersion

Змінна swaggerVersion оголошена в кореневому скрипті:

   ext {
      swaggerVersion = "2.2.8"
   }

Тут блок ext {} — це синонім оголошення project.ext, і коли ви в Groovy звертаєтеся до свого swaggerVersion, то це ж саме, що б ви написали project.ext.swaggerVersion. У Kotlin це працює трохи інакше. Зрозуміло, він підтримує string templates, але не підтримує блок ext {}. Які ж є альтернативи?

По-перше, можна просто оголосити змінну у скрипті:

val swaggerVersion = "2.2.8"

Такий підхід не дуже хороший, тому що це глобальна змінна, до якої всі можуть мати доступ. А ООП вчить нас намагатися обмежувати область видимості.

Другий підхід — створити Singleton та оголосити там версії як константи:

object Versions {
    const val swaggerVersion = "2.2.8"
}

Тоді оголошення залежності буде виглядати трохи інакше:

    api("io.swagger.core.v3:swagger-annotations:${Versions.swaggerVersion}")   

Третій підхід — оголосити всі версії глобальних залежностей в окремому тестовому файлі gradle.properties:

# Dependency versions
swaggerVersion=2.2.8

Правда саму змінну все ж таки доведеться оголосити (як делегована властивість):

val swaggerVersion:String by project

Четвертий варіант використовує так звані Extra properties, схожі на ті, які ми оголошували в Groovy:

extra["swaggerVersion"] = "2.2.8"

Щоправда, використовувати їх більш громіздко, тому що доводиться додавати щоразу pro-ject.extra:

   api("io.swagger.core.v3:swagger-annotations:${project.extra["swaggerVersion"]}")   

На жаль, це плата за втрату гнучкості, яка була у Groovy. Якщо розглянути всі чотири варіанти з точки зору простоти міграції, то оптимальнішим здається розмістити версії в gra-dle.properties, а потім звертатися до них через делегування.

Перейдемо до наступних скриптів. При їх збираннi отримуємо помилку при імпорті плагіна:

id "me.champeau.gradle.jmh" version "0.5.3" apply true
   ^ Unexpected tokens (use ';' to separate expressions on the same line)

Тут теж потрібно замінити прогалину на дужку:

plugins {
     id("me.champeau.gradle.jmh") version "0.5.3" apply true
}

Переходимо до скрипту для war-проєкту. Тут одразу падає збірка на використанні плагіна:

apply plugin: 'war'
 ^ Function invocation 'apply()' expected

Потрібно просто оголосити плагін за допомогою блоку plugins {}:

plugins {
       war
}

Далі блок оголошень змінних:

ext {
   elVersion = '3.0.0'
   weldVersion = '5.1.0.Final'
   metricsVersion = '4.2.17'  
   resilience4jVersion = '2.0.2'
}

який проситься замінити на:

const val elVersion="3.0.0"
const val weldVersion="5.1.0.Final"
const val metricsVersion="4.2.17"  
const val resilience4jVersion="2.0.2"

Але, на жаль, це не проходить, тому що

Const 'val' are only allowed on top level, in named objects, or in companion objects

Тому доведеться обійтися просто val. У цьому скрипті в нас є Groovy-функція, яка точно буде забракована компілятором Kotlin:

def isWildflyDependency(File file) {
    return(["jboss", "weld", "cdi", "javax.faces", "validation-api", "javax.el"].findIndexOf { file.getName().contains(it) } >= 0
}

Її потрібно просто переписати як:

fun isWildflyDependency(file: File): Boolean {
    return arrayOf("jboss", "weld", "cdi", "javax.faces", "validation-api", "javax.el").find({ file.getName().contains(it) }) != null
}

Далі йде конфігураційний блок для War-плагіна, який не підходить у Kotlin-варіанті:

war {
^ Expression 'war' cannot be invoked as a function. The function 'invoke()' is not found

Тут доведеться додати префікс tasks (який опускається для Groovy-скриптів):

tasks.war {

У його тілі виклик методу isWildflyDependency тепер не компілюється:

    classpath = classpath.filter { !project.hasProperty("wildfly") || !isWildflyDependency(it) }
  ^ Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type FileCollection?

Змінна classpath належить до nullable типу, тому доведеться додати ? для безпечного виконання:

    classpath = classpath?.filter { !project.hasProperty("wildfly") || !isWildflyDependency(it) }

Далі йде сторонній репозиторій:

repositories {
       maven { url "https://repository.primefaces.org" }
}

Тут потрібно викликати maven як функцію:

repositories {
       maven(url="https://repository.primefaces.org")
}

У Groovy теж є свій спосіб іменування значень аргументів (за допомогою двокрапок):

      ant.native2ascii(
         src   : project.file("src/main/resources/txt"),
         dest  : project.file("src/main/resources/ui")
      )

Переробимо цей блок під Kotlin:

      ant.native2ascii(
         src = project.file("src/main/resources/txt"),
         dest = project.file("src/main/resources/ui")
      )

Остання проблема в цьому скрипті — відсутність методу native2ascii в об’єкті ant:

^ Unresolved reference: native2ascii

Виникає питання, а що це за об’єкт ant і звідки він узявся? Ми його ніде не оголошували. Насправді цей об’єкт неявно створюється під час складання, його тип — AntBuilder. По суті, він є обгорткою над типом AntBuilder з Groovy. Але найцікавіше інше: методу native2ascii справді немає в цьому класі, й абсолютно незрозуміло, чому це працювало в Groovy скрипті. Одне з пояснень: native2ascii є назвою завдання в файлі Ant, і Groovy спеціальними способами його викликає. Я додав тикет на цю проблему, зачекаємо на відповідь від офіційних осіб.

Але як же викликати цю функцію? Можна спробувати оголосити Ant завдання, ініціалізувати та викликати її:

import org.apache.tools.ant.taskdefs.optional.Native2Ascii
 
val task = Native2Ascii()
task.setSrc(project.file("src/main/resources/txt"))
task.setDest(project.file("src/main/resources/ui"))
task.execute()

На жаль, отримуємо помилку:

Cannot invoke "org.apache.tools.ant.Project.log(String, int)" because "p" is null

Тому поки що відкладемо цю частину та йдемо далі. У Spring Boot-проєктах також не все гладко. По-перше, apply from елемент (для імпорту іншого скрипта) більше не працює та видає відразу кілька помилок:

apply from: "../gradle-scripts/build-spring-boot.gradle"
^ Function invocation 'apply()' expected
^ Not enough information to infer type variable T

Доводиться переробляти apply у виклик функції:

apply(from ="../gradle-scripts/build-spring-boot.gradle")

Далі переробити на функцію:

    runtimeOnly("io.jsonwebtoken:jjwt-jackson:${jwtVersion}")
      { exclude group: "com.fasterxml.jackson.core", module: "jackson-databind" }

виклик exclude:

    runtimeOnly("io.jsonwebtoken:jjwt-jackson:${jwtVersion}")
      { exclude(group="com.fasterxml.jackson.core", module= "jackson-databind") }

Потім йде блок конфігурації, який перейменовує результуючий jar-файл:

bootJar {
   archiveFileName = "gateway.jar"
}
^ Expression 'bootJar' cannot be invoked as a function. The function 'invoke()' is not found

Цікаве тут те, що приклад із Reference Guide виявився неробочим:

tasks.named<BootJar>("bootJar") {
   archiveFileName.set("gateway.jar")
}

Після деякого дослідження з’ясувалося, що правильним буде такий блок:

tasks.bootJar {
   archiveFileName.set("gateway.jar")
}

Але головні проблеми попереду. Чомусь Gradle взагалі відмовляється компілювати скрипт, який ми імпортуємо:

apply(from ="../gradle-scripts/build-spring-boot.gradle.kts")

З помилок випливає, що він не бачить там конфігурацію (implementation) з Java-плагіна, який був оголошений у скрипті-контейнері:

implementation(platform("org.springframework.cloud:spring-cloud-dependencies:$cloudStarterVersion"))
   ^ Unresolved reference: implementation

Помилка не зникає, якщо в скрипті, що імпортується, вказати Spring Boot-плагін:

plugins {
     id("org.springframework.boot") version "3.0.5" apply true
}

Спробуємо зібрати build-spring-boot-gradle.kts окремо від проєкту:

gradle -b build-spring-boot-gradle.kts

Скрипт збирається успішно. Причина такої поведінки незрозуміла, я створив тикет на цю помилку, а поки що назад повернув Groovy-скрипт build-spring-boot.gradle та імпортував саме його (з Kotlin):

apply(from ="../gradle-scripts/build-spring-boot.gradle")

Пізніше я отримав відповідь від інженерів Gradle щодо цієї проблеми. Виявилося, що в Kotlin DSL є два типи використання моделі та конфігурації: type-safe та type-unsafe. Коли ми вказуємо плагіни через plugins {}, можемо використовувати type-safe-конфігурацію, хоча назва конфігурації (api) визначається лише runtime:

    api("io.swagger.core.v3:swagger-annotations:2.2.8")   

Але якщо ми імпортуємо один скрипт з іншого, то нам доводиться використовувати type-unsafe-підхід і вказувати назви властивостей та методів як рядки:

    "api"("io.swagger.core.v3:swagger-annotations:2.2.8")   

Такий варіант є гіршим, тому ми залишимо чинний підхід з імпортом Groovy-скрипта.

Переходимо до скрипту збирання для iншого сервісу. Тут не компілюється блок конфігурації для Spring REST Docs:

test {
   outputs.dir snippetsDir
}

Перепишемо його як:

tasks.test {
   outputs.dir(snippetsDir)
}

Ще один рядок, який більше не компілюється, — це визначення залежності між задачами (tasks):

dependsOn test

Що у Kotlin буде мати такий вигляд:

dependsOn(tasks.test)

Складніше буде з конфігурацією JVM-аргументів для тесту. Річ у тому, що з цього блоку не зрозуміло, який тип властивостей jvmArgs:

test {
   jvmArgs '-Djersey.config.test.container.port=9997' 
}

Спочатку здається, що це рядок, але з документації випливає, що все це список, і навіть одиничний елемент потрібно перетворити на список за допомогою listOf:

tasks.test {
   jvmArgs(listOf("-Djersey.config.test.container.port=9997")) 
}

Переходимо до сервісу, який використовує Micronaut. Тут у нас окремий конфігураційний блок для GraalVM:

graalvmNative {
    binaries {
        main {
            sharedLibrary = false
            buildArgs.add("--libc=musl")
            buildArgs.add("--static")
        }
    }
}

який компілюється з помилкою:

^ Expression 'main' cannot be invoked as a function. The function 'invoke()' is not found

Думаю, що попередньо мало хто скаже, який вигляд такий блок матиме в Kotlin варіанті. А ось документація підказує, що потрібно main перетворити на named("main"):

graalvmNative {
    binaries {
        named("main") {
            sharedLibrary.set(false)
            buildArgs.add("--libc=musl")
            buildArgs.add("--static")
        }
    }
}

named — це метод колекції tasks, який повертає завдання за назвою. Перейдемо до клієнтської програми. Тут ми використали свій клас:

@CacheableTask
public abstract class CacheableYarnTask extends com.github.gradle.node.yarn.task.YarnTask {
       @InputFiles
       @PathSensitive(PathSensitivity.ABSOLUTE)
       FileCollection sourceFiles = project.files("client/src", "client/package.json", "client/angular.json")
 
       @OutputDirectory
       File outputDir = project.file("client/dist")   
}

який потрібно переробити під синтаксис Kotlin:

@CacheableTask
public abstract class CacheableYarnTask() : com.github.gradle.node.yarn.task.YarnTask() {
       @InputFiles
       @PathSensitive(PathSensitivity.ABSOLUTE)
       val sourceFiles = project.files("client/src", "client/package.json", "client/angular.json")
 
       @OutputDirectory
       val outputDir = project.file("client/dist")   
}

Винятковий момент, коли код у Kotlin вийшов меншим через те, що в ньому можна не вказувати тип, якщо оголошення змінної поєднується з ініціалізацією. Водночас зручність Groovy в тому, що для вказівки залежності між завданнями просто написати:

assemble.dependsOn(tasks.named("buildAngular"))

У Kotlin же доведеться dependsOn обернути в лямбду:

       tasks.assemble {
          dependsOn(tasks.named("buildAngular"))
       }

Начебто все працює успішно, тільки ось це оголошення завдання:

    tasks.register<CacheableYarnTask>("buildAngular", CacheableYarnTask) {
              dependsOn(tasks.named("yarn_install"))
              args = listOf("run","build")
       }

призводить до дивної помилки:

^ Classifier 'CacheableYarnTask' does not have a companion object, and thus must be initialized here

Річ у тому, що спочатку цей блок мав такий вигляд:

    tasks.register("buildAngular", CacheableYarnTask) {
                                dependsOn tasks.named("yarn_install")
                                args = ['run','build']
                }

Яку б мову ви не використовували, Gradle вимагає вказати тип завдання під час її створення. У Groovy можна вказати просто назву типу (CacheableYarnTask), але Kotlin звичніше передати тип як generic-аргумент:

tasks.register<CacheableYarnTask>

І тоді можна не вказувати другий аргумент:

tasks.register<CacheableYarnTask>("buildAngular") {

Аналогічного результату можна було б досягти й без generics, але передавши клас як другий аргумент:

       tasks.register("buildAngular", CacheableYarnTask::class) {

Тепер лишилося оновити кореневий скрипт. Тут потрібно змінити оголошення версій:

   sourceCompatibility = 1.17
   targetCompatibility = 1.17

на явне використання Java-плагіна та спеціального перерахування JavaVersion:

   java {
       sourceCompatibility = org.gradle.api.JavaVersion.VERSION_17
       targetCompatibility = org.gradle.api.JavaVersion.VERSION_17
   }  

Тепер міграція здається закінченою, але все ще залишився блок Ant-коду, який не хоче компілюватись у Kotlin. Тут є два варіанти вирішення. Перший (найпростіший і найбезпечніший з точки зору міграції): перенести його в окремий Groovy-скрипт ant.build.gradle:

processResources {
 
  doFirst {
      ant.native2ascii(
         src : project.file("src/main/resources/txt"),
         dest : project.file("src/main/resources/ui")
      )
   }
   exclude("txt", "docker", "admin.xml")
}

Й імпортувати його з Kotlin-скрипту:

apply(from="ant.build.gradle")

Другий варіант для тих, хто хоче позбавитися повністю від Groovy коду: використовувати метод withGroovyBuilder, який дозволяє динамічно звертатися до Groovy-коду (у нашому випадку до методів та властивостей об’єкта ant:

ant.withGroovyBuilder {                                         
        "native2ascii"("src" to project.file("txt"), "dest" to project.file("ui"))
}

Зверніть увагу, що назви та значення аргументів передаються як пари ключ — значення в Map.

Перезбираємо проєкт, і тоді складання відбувається успішно. У чому ми виграли, перейшовши на Kotlin? Перевіримо два досить важливі показники: час збирання та розмір скриптів. Gradle за замовчуванням кешує артефакти збирання, тому для збiрки проєкту з нуля використовуємо опцію --no-build-cache:

gradle —no-build-cache clean assemble

Отримуємо:

  1. Kotlin — 19 секунд
  2. Groovy — 18 секунд

Тепер перейдемо до загального розміру скриптів:

  1. Groovy — 16.8 КБ
  2. Kotlin — 17.6 КБ

Як ви бачите, після міграції ми не втратили в продуктивності, але приблизно на 5% втратили в обсязі. У цьому є іронія, тому що однією з переваг Gradle (у порівнянні з Maven) була його компактність. І ось тут ми трохи нею пожертвували.

Висновки

Якщо підбивати підсумки міграції, то вона пройшла успішно, хоч і не без проблем. Було знайдено дві помилки, які свідчили про те, що синтаксис та можливості Kotlin DSL відрізняються від аналогічних у Groovy DSL. Міграція вимагає хорошого знання Groovy/Kotlin і DSL. Водночас ви можете в одному проєкті поєднувати Groovy/Kotlin скрипти і навіть імпортувати Groovy код з Kotlin, що допомогло нам вирішити наші проблеми. З Kotlin ми отримали статичну типізацію, але втратили легкість Groovy, яка дозволяла писати більш простий читабельний код.

Чи варто переходити з Groovy на Kotlin? Вирішувати вам, але вже зараз можна порадити писати в тому стилі, який притаманний обом мовам і в майбутньому полегшить міграцію. Памʼятаємо про:

  • лапки, а не апострофи;
  • використання дужок для виклику функцій.

Не забувайте, що Gradle поки що підтримує Groovy, але ця підтримка може закінчитися будь-якої миті. Чомусь мені здається, що підтримка одразу двох мов вимагає надто багато зусиль і створює надто багато багів. У Gradle репозиторії на Github зараз понад 2 300 відкритих тикетів, і їхня кількість лише зростає. Але навіть якщо підтримка Groovy залишиться, для роботи зі скриптами збірки на ньому потрібна людина, яка добре знається на Groovy, а таку знайти складніше, ніж Kotlin-розробника.

👍ПодобаєтьсяСподобалось1
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Смисл з цього всього якщо є maven

+
проекти з maven просто працють роками (десятиліттями), а проекти з gradle ламаються з кожною версією.

Давно мігрував багатомодульні андроїд проекти як тільки градл випустив новину, що вони переходять на котлін — якщо з груві це був постійний пошук магічного спелу на стековерфлоу, то з котлінскріпт ти отриумеєш автокомпліт, статичну генерацію типів, наприклад implementation(projects.common) без усіляких строкових констант. Плюс якщо груві не працює, це дізнаєшся тільки при білді, зараз же більшість помилок тобі підсвітить IDE, а рантайм помилок значно поменшало і текст помилки доволі інформативний. У комбінації з toml файлом опису залежностей зараз рулити білдом набагато краще. Ще з плюсів — коли потрібно написати якийсь shell like скрипт, робити на котліні зручніше, бо не потрібно постійно вчити groovy, який ніде більше не використовується у проекті. Також зараз є configuration-cache яке з груві неможливо бо це природа dynamic language.
Якщо раніше градл скрипти то була біль оновлювати, то зараз це просто звичайна таска)

Так і не зрозуміло, навіщо переходити. Хіба що розробники не знають groovy, але ж білд скрипти однотипні: можна просто скопіювати з іншого проєкту та робити все по аналогії.

Ну давайте ще в Україні російські технології просувати...

чеські
у інтела/боінга були дев центри в рашці, але проци/літаки не стали свинособачими :)

Ну, наврядчи хтось перевіряє, чи повивозили jetbrains всіх своїх розробників з рашки, та чи розірвали контракти з тими, хто виїхати не захотів. З одного боку, тулзи JetBrains зручні, з іншого, вони не єдині в їх області.

До чого тут дев центри? JetBrains була заснована в Санкт-Петербурзі, там менеджмент звідти і спочатку все розроблялось там, потім частина з них переїхала. Навіть Котлін — це назва острову поряд з цим містом.

Пропонуєте кенселити всі(99%) додатки на андроїд які були розроблені з 2018року, гугл, і всіх хто використовує IDE та сервіси від JetBrains? Можливо ще й сервіси в яких на бекенді котрін використовують.

Ні, пропоную не просувати їх на українських ресурсах.

Палестинці порізали купу народу, палестинці — араби, то відмовся від арабських цифр і юзай римські.

Ну, насправді ці цифри — індійські, але хто ж такими речами цікавиться, правда?

1) Ці цифри винайшли індуси. 2) це було дуже давно. 3) JetBrains є зараз і його засновники конкретні російські міліардери. Вони мають неабиякий зиск від просування Котліну. І один з них точно живе в Санкт-Петербурзі і платить там податки. Як на мене, це мало відрізняється від покупки російської музики чи просмотру російських каналів на YouTube.

Може, краще навпаки? З котліну — на груві? Бо котлін же ж створили росачки. І, до речі, котлін — це «ісссконнна-русссская» назва острову на росії, у Фінській затоці, 100% в когось віджали і не стали переназивати навіть.

Пробував мігранути на котлін ще рік тому один з сервісів, вперся в те, що багато плагінів з котліном не дружать зовсім та не підтримується імпорт скріптів. Зараз з цим краще?

Що ви маєте на увазі — плагіни не дружать з Kotlin? Які саме та в чому це виражається?

Гредл плагіни деяких сторонніх бібліотек не підтримувались на той час

Так я так і не почув, отаке працює вже
apply from: "gradle/folder/script-to-import.gradle"?

Підписатись на коментарі