Блог Roxie Mobile

Размещение библиотеки через JCenter и Maven Central при помощи Android Studio

19 февраля 2018 747
Android разработка

Если в Android Studio нужно подключить библиотеку к проекту, то достаточно просто добавить следующую строчку с зависимостью в файл build.gradle модуля.

1
2
3
dependencies {
compile 'com.inthecheesefactory.thecheeselibrary:fb-like:0.9.3'
}

И всё. Библиотеку можно использовать.
Это очень круто. Однако возникает вопрос: откуда же Android Studio берёт библиотеки? В этой статье детально описано, как это работает, в том числе, как опубликовать свою собственную библиотеку и поделиться ей с разработчиками по всему миру.

Откуда Android Studio берёт библиотеку?

Начнём с этого, на первый взгляд, простого вопроса. Думаю, не все знают наверняка. Возможно, Android Studio просто гуглит для нас библиотеку и загружает в проект?
К счастью, всё не так сложно. Android Studio загружает библиотеку с сервера Maven-репозитория, заданного в build.gradle. (Apache Maven – это инструмент, разработанный Apache, представляющий собой файловый сервер для публикации Java библиотек). По сути, есть всего 2 популярных сервера, используемых для размещения библиотек для Android — это JCenter и Maven Central.

JCenter

JCenter — это Maven-репозиторий, размещённый на сайте Bintray.com. Сам репозиторий расположен по этой ссылке.
Чтобы использовать JCenter в проекте, нужно прописать репозиторий в файле build.gradle, как показано ниже:

1
2
3
4
5
allprojects {
repositories {
jcenter()
}
}

Maven Central

Maven Central — это Maven-репозиторий, размещённый на сайте Sonatype.org. Сам репозиторий расположен по этой ссылке.
Чтобы использовать Maven Central в проекте, нужно прописать репозиторий в файле build.gradle, как показано ниже:

1
2
3
4
5
allprojects {
repositories {
mavenCentral()
}
}

Обратите внимание, хотя и JCenter, и Maven Central являются типовыми репозиториями библиотек для Android, они расположены на совершенно разных площадках от разных провайдеров и никак не связаны друг с другом. То, что доступно в JCenter, может отсутствовать в Maven Central, и наоборот.

Помимо этих двух стандартных серверов, мы можем указывать произвольные сервера Maven-репозиториев, если, например, используем библиотеку, размещённую на собственном сервере. Например, сервис Fabric.io использует свой репозиторий, расположенный по адресу https://maven.fabric.io/public. Если хотите использовать какую-нибудь из библиотек Fabric.io, то придётся самостоятельно указать URL репозитория следующим образом:

1
2
3
repositories {
maven { url 'https://maven.fabric.io/public' }
}

После этого вы сможете подключить к проекту любые библиотеки Fabric.io:

1
2
3
dependencies {
compile 'com.crashlytics.sdk.android:crashlytics:2.2.4@aar'
}

Что лучше: разместить свою библиотеку на стандартном сервере или поддерживать собственный? Лучше первое — это позволит сделать библиотеку доступной широкой публике. Другому разработчику потребуется лишь дописать строчку кода с названием зависимости. Поэтому в этой статье мы сосредоточимся на JCenter и Maven Central, использовать которые гораздо удобнее.

Кстати, помимо Maven есть ещё один тип репозиториев, который работает с gradle в Android Studio. Это репозиторий Ivy, однако за свою практику я никогда не видел, чтобы кто-то его использовал (включая меня), поэтому в этой статье просто не буду его рассматривать.

Понимание JCenter и Maven Central

Почему существует не один, а целых два типовых репозитория?

На самом деле, оба они выполняют одну и ту же функцию: хранят библиотеки Java/Android. А дальше дело разработчика – размещать библиотеки на одном из них или, быть может, на обоих.

Поначалу Maven Central был в Android Studio репозиторием по умолчанию. Когда вы создаете новый проект в старой версии Android Studio, mavenCentral() автоматически задаётся в build.gradle.

Основная проблема Maven Central заключается в том, что он неудобен для разработчиков. Опубликовать библиотеку там является нетривиальной задачей. По этой и ряду других причин (например, проблемы с безопасностью и т.п.) команда разработчиков Android Studio решила сменить репозиторий по умолчанию на JCenter. Теперь, когда вы создадите проект в новой версии Android Studio, то увидите, что репозиторием по умолчанию задан именно jcenter(), а не mavenCentral().

Есть много весомых причин, из-за которых был произведён переход от Maven Central к JCenter. Приведём основные:

  • JCenter предоставляет библиотеки через CDN, что позволяет быстро их скачивать;
  • JCenter — это самый большой репозиторий Java в мире. Всё, что доступно через Maven Central, скорее всего доступно и через JCenter;
  • Очень просто опубликовать свою библиотеку. Её не требуется подписывать или делать ещё что-то нетривиальное, в отличие от Maven Central;

Интуитивно понятный интерфейс;

Если вы решите опубликовать свою библиотеку в Maven Central, вы сможете легко сделать это всего одним кликом на сайте Bintray (после небольшой предварительной настройкой в первый раз).

С учётом сказанного ранее, а также из личного опыта, должен сказать, что смена репозитория по умолчанию на JCenter — замечательное решение.

Поэтому в этой статье мы сосредоточимся на JCenter, так как после успешной загрузки вашей библиотеки в JCenter ее можно сразу отправить в Maven Central.

Как gradle получает библиотеку из репозитория

Прежде чем мы рассмотрим, как публиковать библиотеки в JCenter, разберемся с тем, как gradle получает библиотеки из репозитория. К примеру, когда мы пишем такую строчку в build.grade, как именно файлы библиотеки попадают в проект?

1
compile 'com.inthecheesefactory.thecheeselibrary:fb-like:0.9.3'

Сначала нам нужно знать формат строки библиотеки. Она состоит из 3 частей:

1
GROUP_ID:ARTIFACT_ID:VERSION

В нашем случае GROUP_ID это com.inthecheesefactory.thecheeselibrary, ARTIFACT_ID имеет значение fb-like, а VERSION равен 0.9.3.

Для большей ясности, GROUP_ID задаёт название группы библиотек. Бывает такое, что имеет смысл сгруппировать в одном контексте несколько библиотек, выполняющих различные задачи. Если библиотеки входят в одну группу, то их GROUP_ID будет совпадать. Обычно GROUP_ID – это название разработчика пакета, после которого идёт название группы библиотек, например, com.squareup.picasso. Затем задаётся настоящее название библиотеки в ARTIFACT_ID. Что касается VERSION, то здесь хранится только номер версии. Несмотря на то, что в это поле можно ввести произвольный текст, лучше задавать его в формате x.y.z с -beta на конце, если нужно.

Ниже приведён реальный пример библиотеки, взятый из Square. В каждой строке легко обнаружить и название библиотеки, и название разработчика.

1
2
3
4
5
6
dependencies {
compile 'com.squareup:otto:1.3.8'
compile 'com.squareup.picasso:picasso:2.5.2'
compile 'com.squareup.okhttp:okhttp:3.9.1'
compile 'com.squareup.retrofit:retrofit:2.3.0'
}

Что произойдет, когда мы добавим зависимость как в примере выше? Всё просто. Gradle спросит сервер Maven-репозитория, существует ли такая библиотека, и если да, то получит путь к запрашиваемой библиотеке. Чаще всего он будет в формате GROUP_ID/ARTIFACT_ID/VERSION_ID. Например, файлы библиотеки com.squareup:otto:1.3.8 можно найти по ссылкам https://jcenter.bintray.com/com/squareup/otto/1.3.8/ и https://oss.sonatype.org/content/repositories/releases/com/squareup/otto/1.3.8/.

Затем Android Studio загрузит эти файлы на ваш компьютер и добавит их в проект. И всё. Ничего сложного!

Надеюсь, сейчас вы чётко понимаете, что скачиваемые библиотеки – это всего лишь файлы формата jar или aar, размещённые на сервере репозитория. По сути, если вы скачаете файлы сами, скопируете и добавите их в проект, получится то же самое. Но огромный плюс системы зависимостей в gradle — это отсутствие необходимости делать что-либо дополнительно, кроме как набрать несколько строчек текста. Библиотека сразу же становится доступной в вашем проекте, причём с системой управления версиями.

Что такое формат aar

Погодите… Я упомянул, что есть два типа файлов, которые размещают в репозитории: jar и aar. С jar файлами, думаю, всё понятно, но что именно представляет собой aar?

Формат aar разработан на основе jar. Доработка потребовалась потому, что в библиотеках для Android должны присутствовать особые файлы, такие как AndroidManifest.xml, Resources, Assets и JNI, которых нет в обычных jar-файлах. Поэтому был изобретён формат aar, решающий все эти проблемы. По сути, это обычный zip-архив, как и jar, только с другой файловой структурой. Файл jar расположен внутри файла aar под именем classes.jar. Остальное перечислено ниже:

  • /AndroidManifest.xml (обязательно)
  • /classes.jar (обязательно)
  • /res/ (обязательно)
  • /R.txt (обязательно)
  • /assets/ (необязательно)
  • /libs/*.jar (необязательно)
  • /jni//*.so (необязательно)
  • /proguard.txt (необязательно)
  • /lint.jar (необязательно)

Как видите, формат файла aar специально разработан для Android. А эта статья научит вас создавать и публиковать библиотеки в формате aar.

Как загрузить библиотеку в JCenter

Теперь, когда мы ознакомились с основными принципами работы репозиториев, приступим к самому важному — процессу загрузки. Его суть проста: разместить файлы библиотеки по адресу https://jcenter.bintray.com. Как только мы это сделали — библиотека опубликована. Есть две задачи с которыми необходимо разобраться: как создать aar-файл и как разместить собранные файлы в репозитории.

И хотя для сборки и публикации требуется много шагов, это вовсе не сложно, т.к. Bintray неплохо к этому подготовлен. Весь процесс можно увидеть на диаграмме ниже:

Из-за большого количества деталей я разобью процесс на 7 частей, чтобы описать все действия по шагам.

Часть 1: Создаём пакет на сайте Bintray

Чтобы создать пакет на сайте Bintray, сделайте следующее:
Шаг 1: Зарегистрируйтесь на Bintray.com. (Процесс регистрации весьма прост, так что сделайте это самостоятельно)
Шаг 2: После регистрации авторизуемся на сайте и нажимаем на maven:

Шаг 3: Выбираем Add New Package и создаём новый пакет для нашей библиотеки:

Шаг 4: Заполняем всю обязательную информации:


Несмотря на то, что в поле Package Name можно ввести произвольный текст, есть несколько общепринятых правил о том, как называть пакеты: используйте символы в нижнем регистре и разделяйте слова с помощью дефиса (-). Например, fb-like.

После заполнения обязательных полей, нажимаем Create Package.

Шаг 5: Веб-сайт перенаправит вас на страницу Edit Package. Щёлкаем по имени пакета под Edit Package и переходим на страницу с информацией о пакете:

Готово! Теперь у нас есть собственный Maven-репозиторий на Bintray, в который можно загрузить библиотеку.

Подготовка аккаунта на Bintray завершена. Теперь переходим к Sonatype и их репозиторию Maven Central.

Часть 2: Создаём учётную запись Sonatype для Maven Central

Примечание: если вы не собираетесь размещать библиотеку в Maven Central, то можно пропустить части 2 и 3. Однако, советую не делать этого, потому что многие разработчики всё ещё используют этот репозиторий.

Аналогично JCenter, если вы хотите распространять свою библиотеку через Maven Central, предварительно необходимо создать аккаунт на сайте Sonatype. Для этого переходим на страницу Sonatype Dashboard и регистрируемся. Важно понимать, что аккаунт, который вы создадите — это учетная запись JIRA Issue Tracker на сайте Sonatype.

После этого нужно запросить разрешение на публикацию библиотеки в Maven Central. Процедура бессмысленная (по крайней мере на мой взгляд), ведь нужно создать тикет в JIRA, чтобы вам разрешили загрузить библиотеку, GROUP_ID которой совпадает с заданным в Maven Central.

Чтобы создать такой тикет, заходим на страницу Sonatype Dashboard и авторизуемся под созданным аккаунтом. Затем в верхнем меню нажимаем Create.

Заполняем следующую информацию:

Project: Community Support — Open Source Project Repository Hosting

Issue Type: New Project

Summary: Сокращённое название вашей библиотеки, например, The Cheese Library

Group Id: Указываем корневой GROUP_ID, например, com.inthecheeselibrary. После того, как запрос одобрят, можно начать добавлять в репозиторий библиотеки, GROUP_ID которых начинается с com.inthecheeselibrary , например, com.inthecheeselibrary.somelib.

Project URL: Указываем URL библиотеки, которую планируем публиковать, например: https://github.com/nuuneoi/FBLikeAndroid

SCM URL: URL системы контроля версий, например: https://github.com/nuuneoi/FBLikeAndroid.git

Остальное не трогаем и нажимаем Create. На этом всё. Теперь переходим к самому сложному. Нужно дождаться, когда нам предоставят доступ для размещению библиотеки в Maven Central. В среднем это занимает порядка недели или чуть больше.

Последнее, что необходимо сделать — это указать имя пользователя Sonatype OSS на вкладке Accounts раздела Bintray Profile:

Нажимаем Update и на этом настройка завершена.

Часть 3: Включаем автоматическое подписывание в Bintray

Как упоминалось ранее, через JCenter можно добавить библиотеку в Maven Central, но для этого сначала её нужно подписать. Через веб-интерфейс Bintray легко сделать так, чтобы библиотека автоматически подписывалась после загрузки.

Сначала, с помощью командной строки, генерируем ключ следующим образом (если работаете под Windows, то используйте Cygwin):

1
gpg --gen-key

Часть полей обязательна для заполнения. В большинстве случаев подойдут значения по умолчанию, но часть полей требуется заполнить самостоятельно. Например, настоящее имя, пароль и тому подобное.

И так, ключ создан. Выполним следующую команду, чтобы посмотреть информацию о нем:

1
gpg --list-keys

Если всё в порядке, то отобразится информация, похожая на ту, что приведена ниже:

1
2
3
pub 2048R/01ABCDEF 2015-03-07
uid Sittiphol Phanvilai <yourmail@email.com>
sub 2048R/98765432 2015-03-07

Теперь нам нужно загрузить открытый ключ на сервер ключей. Для этого выполните следующую команду, предварительно заменив PUBLIC_KEY_ID 8-значным шестнадцатеричным значением после 2048R/ в строке pub, которая в этом примере равна 01ABCDEF.

1
gpg --keyserver hkp://pool.sks-keyservers.net --send-keys PUBLIC_KEY_ID

Теперь экспортируйте открытый и закрытый ключи в формат ASCII-Armor, заменив yourmail@email.com на адрес, указанный при создании ключа на предыдущем шаге.

1
2
gpg -a --export yourmail@email.com > public_key_sender.asc
gpg -a --export-secret-key yourmail@email.com > private_key_sender.asc

Открываем страницу Edit Profile на сайте Bintray и щёлкаем по GPG Signing. Заполняем поля Public Key и Private Key содержимым из полученных на предыдущем шаге файлов public_key_sender.asc и private_key_sender.asc соответственно.

Сохраняем ключи, нажав Update.

Заключительный шаг – включаем автоматическую подпись. Для этого переходим на главную страницу Bintray и нажимаем на maven.

Нажимаем Edit.

Активируем галочку GPG Sign uploaded files automatically, тем самым включая автоматическую подпись.

Нажимаем Update, чтобы сохранить изменения. Вот и всё. С этого момента любая библиотека, добавленная в наш Maven-репозиторий, будет автоматически подписана, и можно будет одним щелчком мышки опубликовать её ещё и в Maven Central.

Обратите внимание, что эту настройку требуется произвести вручную лишь один раз. Ко всем загруженным в дальнейшем библиотекам они будут применяться автоматически.

Работа с Bintray и Maven Central закончена. Переходим к Android Studio.

Часть 4: Подготавливаем проект в Android Studio

Во многих случаях нам может понадобиться загрузить в репозиторий сразу несколько библиотек из одного проекта. Также возможно, что нам наоборот потребуется что-то не загружать. Поэтому рекомендую выделить каждую часть в отдельный модуль. Проще говоря, разбить приложение по меньшей мере на 2 модуля: Application Module с примером использования библиотеки и Library Module с исходным кодом библиотеки, которую надо разместить в репозитории. Заметьте, что, если в вашем проекте более одной библиотеки, вы можете добавить ещё модулей: по одному на библиотеку.

Создать модуль библиотеки достаточно просто, поэтому не будем углубляться в эту процедуру. Просто создайте модуль типа Android Library и всё.

Затем добавляем в проект плагин Bintray. Для этого следует отредактировать файл build.gradle проекта следующим образом.

1
2
3
4
5
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.0'
classpath 'com.github.dcendents:android-maven-gradle-plugin:2.0'
}

Далее в файле local.properties задаём имя пользователя и ключ API для аутентификации на Bintray, а также пароль созданного ключа. Эти данные конфиденциальны и должны находиться в отдельном файле, чтобы их можно было скрыть от посторонних глаз, включая систему контроля версий. К счастью, local.properties добавляется в .gitignore при создании проекта. Следовательно, конфиденциальные данные не смогут случайно оказаться на сервере Git.

Добавляем следующие три строчки:

1
2
3
bintray.user=YOUR_BINTRAY_USERNAME
bintray.apikey=YOUR_BINTRAY_API_KEY
bintray.gpg.password=YOUR_GPG_PASSWORD

Первая строка — это имя пользователя Bintray. Вторая — ключ API с вкладки API Key страницы Edit Profile.

Последняя строка — это пароль, который вы задали во время создания ключа GPG на предыдущем шаге. Сохраняем и закрываем файл.

Кроме того необходимо отредактировать файл build.gradle модуля библиотеки. Открываем его и дописываем следующие строки сразу после apply plugin: ‘com.android.library’, как показано ниже.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
apply plugin: 'com.android.library'

ext {
bintrayRepo = 'maven'
bintrayName = 'fb-like'

publishedGroupId = 'com.inthecheesefactory.thecheeselibrary'
libraryName = 'FBLike'
artifact = 'fb-like'

libraryDescription = 'A wrapper for Facebook Native Like Button (LikeView) on Android'

siteUrl = 'https://github.com/nuuneoi/FBLikeAndroid'
gitUrl = 'https://github.com/nuuneoi/FBLikeAndroid.git'

libraryVersion = '0.9.3'

developerId = 'nuuneoi'
developerName = 'Sittiphol Phanvilai'
developerEmail = 'sittiphol@gmail.com'

licenseName = 'The Apache Software License, Version 2.0'
licenseUrl = 'https://www.apache.org/licenses/LICENSE-2.0.txt'
allLicenses = ["Apache-2.0"]
}

Значение bintrayRepo оставляем maven. Изменяем значение bintrayName на название вашего ранее созданного пакета. Остальное правим в соответствии со сведениями о вашей библиотеке. Как видно из скрипта выше, любой сможет использовать вашу библиотеку, добавив следующую строку в свой build.gradle:

1
implementation 'com.inthecheesefactory.thecheeselibrary:fb-like: 0.9.3'

Финальный шаг — воспользуемся двумя скриптами, которые собирают файлы библиотеки и загружают их на Bintray, добавив следующие строки в конец файла build.gradle (для удобства я использую прямые ссылки на файлы на GitHub):

1
2
3
// Place it at the end of the file
apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle'
apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/bintrayv1.gradle'

Отлично! Проект настроен и готов к загрузке на Bintray.

Часть 5: Загружаем библиотеку в свой репозиторий на Bintray

Пора разместить библиотеку в своём репозитории на Bintray. Для этого в Android Studio переходим на вкладку Terminal:

Первым шагом проверяем корректность кода и собираем файлы библиотеки (aar, pom и т.п.). Для этого выполняем следующую команду:

1
> gradlew install

Если всё в порядке, отобразится нечто подобное:

1
BUILD SUCCESSFUL

Полдела сделано. Теперь нужно загрузить собранные файлы на Bintray следующей командой:

1
> gradlew bintrayUpload

Громко кричим «Эврика!», если отобразилось следующее сообщение:

1
SUCCESSFUL

Проверяем пакет через веб-интерфейс Bintray. Вы должны заметить изменения в секции Version.

Переходим по ссылке и на вкладке Files видим список загруженных файлов библиотеки:

Мои поздравления, ваша библиотека опубликована, и ею сможет воспользоваться любой желающий!

Однако не спешите пока особо радоваться. Библиотека всё ещё в вашем Maven-репозитории, а не в JCenter. Если кто-нибудь захочет воспользоваться вашей библиотекой, то ему потребуется сначала прописать URL до вашего репозитория следующим образом:

1
2
3
4
5
6
7
8
9
10
11
repositories {
maven {
url 'https://dl.bintray.com/nuuneoi/maven/'
}
}

...

dependencies {
compile 'com.inthecheesefactory.thecheeselibrary:fb-like:0.9.3'
}

URL Maven-репозитория можно узнать через веб-интерфейс Bintray или просто подставить вместо nuuneoi название своего аккаунта. Рекомендую попробовать открыть данную ссылку в браузере, чтобы понять, как именно всё это работает.

Как уже было сказано ранее, неправильно заставлять разработчиков разбираться и добавлять в свой код такие сложные конструкции. Представьте, для того чтобы подключить 10 библиотек, нужно добавить 10 подобных URL адресов? Кошмар! Давайте перенесём библиотеку из нашего репозитория в JCenter и облегчим людям жизнь.

Часть 6: Синхронизируем репозиторий пользователя на Bintray с JCenter

Синхронизировать свою библиотеку с JCenter очень просто. Заходим в веб-интерфейс и нажимаем Add to JCenter.

Здесь можно просто нажать Send.

Теперь нам остаётся просто подождать 2–3 часа, в течение которых команда Bintray одобрит наш запрос. После того как это произойдёт, вы получите уведомление на Email. Возвращаемся в веб-интерфейс и видим изменения в разделе Linked To.

С этого момента любой разработчик, который использует репозиторий jcenter(), может воспользоваться нашей библиотекой, добавив всего одну строчку в скрипт gradle:

1
implementation 'com.inthecheesefactory.thecheeselibrary:fb-like: 0.9.3'

Хотите убедиться, что ваша библиотека попала в jcenter? Для этого переходим на сайт https://jcenter.bintray.com и открываем папку, название которой состоит из GROUP_ID и ARTIFACT_ID вашей библиотеки. Например, это может выглядеть вот так: com → inthecheesefactory → thecheeselibrary → fb-like → 0.9.3:

Обратите внимание, что привязку к JCenter мы делаем однократно. После этого любое изменение пакета, например, загрузка нового бинарника, удаление старого бинарника и т.п., также отразится и в JCenter. Однако, поскольку ваш репозиторий и JCenter – это не одно и то же, то придётся подождать 2–3 минуты для синхронизации изменений.

И, пожалуйста, будьте осторожны. Если вдруг вы решите удалить пакет целиком, файлы библиотек не исчезнут из JCenter. Они станут зомби-файлами, которые больше никто не сможет удалить. Поэтому, если вы хотите избавиться от всего пакета целиком, то сначала удалите каждую версию библиотеки через веб-интерфейс, и только после этого удалите сам пакет.

Часть 7: Публикуем библиотеку в Maven Central

Не все Android-разработчики используют JCenter. Довольно многие из них продолжают использовать mavenCentral(), поэтому разместим нашу библиотеку также и в Maven Central.

Чтобы отправить библиотеку из JCenter в Maven Central, необходимо выполнить два условия:

    1. Пакет d Bintray должен быть привязан к JCenter;
    1. Репозиторий в Maven Central должен быть одобрен.

Если всё это уже сделано, то отправить библиотеку в Maven Central будет достаточно просто. Нужно всего лишь перейти по ссылке Maven Central на странице пакета.

Ввести имя пользователя и пароль от Sonatype и нажать Sync.

После завершения операции, успешно синхронизированный и закрытый репозиторий появится в разделе Last Sync Status. Если что-то пойдёт не так, то это отобразится в разделе Last Sync Errors. Вам придётся устранять проблемы в каждом случае отдельно, потому что правила размещения у Maven Central весьма строгие.

По завершению, бинарники библиотеки появятся в репозитории Maven Central в каталоге, название которого состоит из GROUP_ID и ARTIFACT_ID библиотеки. В данном случае это: com → inthecheesefactory → thecheeselibrary → fb-like → 0.9.3

Поздравляю! На этом всё. И хотя для публикации библиотеки пришлось проделать множество шагов, они были весьма простые. Большинство из них нужно сделать всего один раз. Больше к ним возвращаться не придётся.

Оригинал статьи: тут

Получайте новости первыми

Вам может быть интересно

Запросить консультацию