Вводная часть (со ссылками на все статьи)

Лет 10-15 назад, когда программы состояли из исходников и небольшого количества двоичных файлов с работой по сборке итоговых программ отлично справлялись всевозможные «?make». Однако сейчас современные программы и подходы к разработке сильно изменились – это:
множество различных файлов (не считаю исходников) – стили, шаблоны, ресурсы, конфигурации, скрипты, бинарные данные и.т.д;

  • предпроцессоры;
  • системы проверки стиля исходников или всего проекта (lint, checkstyle и т.д.);
  • методики разработки, основанные на тестах, с их запуском при сборке;
  • различного типа стенды;
  • системы развёртывания на базе облачных технологий и т.д. и.т.п.

Причём через этапы сборки итоговых или промежуточных артефактов с использованием перечисленных средств и методик среднестатистическому разработчику приходится проходить по несколько раз в день. Запуск командных файлов руками (с возможной проверкой результатов) в данном случае слишком обременительная процедура: необходим инструмент, отслеживающий изменения в ваших данных проекта и запускающий необходимые средства в зависимости от обнаруженных изменений.

Мой путь в использовании систем сборки был непонятное_количество_*make -> ant -> maven -> gradle (то обстоятельство, что android Studio под капотом использует gradle меня сильно порадовало).

Gradle меня привлёк:

  • своей простой моделью (из которой правда можно вырастить монстра, соизмеримого с самим создаваемым продуктом);
  • гибкостью (как в части настройки самих скриптов, так и организации их распределения в рамках больших проектов);
  • постоянным развитием (как и со всеми остальными вещами в разработке — тут надо постоянно изучать что-то новое);
  • лёгкостью адаптации (знание groovy и gradle DSL обязательно);
  • наличием системы plugin’ов, разрабатываемых сообществом — это и разные предпроцессоры, генераторы кода, системы доставки и публикации и прочее, прочее (см. login.gradle.org)

Ознакомиться с возможностями Gradle можно на сайте разработчиков в разделе документации (можно узнать всё!). Для тех, кто хочет сравнить gradle и maven есть интересное видео от JUG.

В моём случае скрипты для сборки выглядят таким образом:

image
,
где:

  • build_scripts/build-tasks.gradle — все задачи для сборки с указанием их зависимостей;
  • build_scripts/dependencies.gradle — описания зависимостей и способов публикации;
  • build.gradle — основной скрипт, определяющий зависимые модули, библиотеки и включающий другие скрипты сборки;
  • settings.gradle — перечень зависимых модулей и настройки самого скрипта (можно переопределить ч/з аргументы запуска gradle).

В такой конфигурации все изменения скрипта сборки производятся в централизованных файлах (не даются на откуп каждому проекту) и могут/должны корректироваться централизованно лицом, ответственным за сборку проекта.

Tips


Из интересных вещей/советов, которыми я хотел бы поделиться, при настройке gradle в моём проекте есть следующие:

  • Вынос версий артефактов (шарится по блокам модулей было скучным занятием)


    Объявляем блок с зависимостями:
    // project dependencies
    ext {
    COMMONS_POOL_VER='2.4.2'
    DROPWIZARD_CORE_VER='1.1.0'
    DROPWIZARD_METRICS_VER='3.2.2'
    DROPWIZARD_METRICS_INFLUXDB_VER='0.9.3'
    JSOUP_VER='1.10.2'
    STORM_VER='1.0.3'
    ...
    GROOVY_VER='2.4.7'
    // test
    TEST_JUNIT_VER='4.12'
    TEST_MOCKITO_VER='2.7.9'
    TEST_ASSERTJ_VER='3.6.2'
    }
    

    И используем его в проекте:
    project(':crawler_scripts') {
    	javaProject(it)
    	javaLogLibrary(it)
    	javaTestLibrary(it)
    
    	dependencies {
    		testCompile "org.codehaus.groovy:groovy:${GROOVY_VER}"
    		testCompile "edu.uci.ics:crawler4j:${CRAWLER4J_VER}"
    		testCompile "org.jsoup:jsoup:${JSOUP_VER}"
    		testCompile "joda-time:joda-time:${JODATIME_VER}"
    		testCompile "org.apache.commons:commons-lang3:${COMMONS_LANG_VER}"
    		testCompile "commons-io:commons-io:${COMMONS_IO_VER}"
    	}
    }
    

  • Вынос настроек во внешний файл
    Создаём или уже имеем файл с конфигурацией:
    ---
    # presented - for test/development only - use artifact from ""/provision/artifacts" directory
    storyline_components:
      crawler_scripts:
        version: "0.5"
      crawler:
        version: "0.6"
      server_storm:
        version: "presented"
      server_web:
        version: "0.1"
    

    И используем его в проекте:
    import com.fasterxml.jackson.databind.ObjectMapper
    import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
    
    buildscript {
    	repositories {
    		jcenter()
    	}
    	dependencies {
    		// reading YAML
    		classpath "com.fasterxml.jackson.core:jackson-databind:2.8.6"
    		classpath "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.8.6"
    	}
    }
    ....
    def loadArtifactVersions(type) {
    	Map result = new HashMap()
    	def name = "${projectDir}/deployment/${type}/hieradata/version.yaml"
    	println "Reading artifact versions from ${name}"
    	if (new File(name).exists()) {
    		ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
    		result = mapper.readValue(new FileInputStream(name), HashMap.class);
    	}
    	return  result['storyline_components'];
    }
    

  • Использование шаблонов

    Это моя любимая часть — позволяет сформировать необходимые конфигурационные файлы из шаблонов.
    Формируем шаблоны:
    version: '2'
    services:
    ...
      server_storm:
        domainname: story-line.ru
        hostname: server_storm
        build: ./server_storm
        depends_on:
            - zookeeper
            - elasticsearch
            - mongodb
        links:
            - zookeeper
            - elasticsearch
            - mongodb
        ports:
         - "${server_storm_ui_host_port}:8082"
         - "${server_storm_logviewer_host_port}:8083"
         - "${server_storm_nimbus_host_port}:6627"
         - "${server_storm_monit_host_port}:3000"
         - "${server_storm_drpc_host_port}:3772"
        volumes:
         - ${logs_dir}:/data/logs
         - ${data_dir}:/data/db
    ....
    

    И используем его в проекте:
    // выполнить копирование скриптов для подготовки сервера
    task copyTemplates (type: Copy, dependsOn: ['createStandDir']){
    	description "выполнить копирование шаблонов"
    	from "${projectDir}/deployment/docker_templates"
    	into project.ext.stand.deploy_dir
    	expand(project.ext.stand)
    	filteringCharset = 'UTF-8'
    }

    В итоге получаете конфигурационный файл, данные которого заполнены значениями переменных, получившими значения в ходе работы скрипта.Однако необходимо отметить, что если вам потребуется более сложная логика с переменными (например скрытие при отсутствии значений) — вам потребуется другой движок шаблонов в системе сборке. Я эту ситуацию обошел используя формат YAML по ходу проекта.

  • Использование списков и замыканий groovy для специфичной обработки конкретных модулей

    Это фактически обычное использование groovy в скрипте сборки, но мне это помогло решить пару нетривиальных задач.

    Объявляем или получаем переменные множественного значения:
    ext {
    	// образы, создаваемые docker'ом
    	docker_machines = ['elasticsearch', 'zookeeper', 'mongodb', 'crawler', 'server_storm', 'server_web']
    	// образы, создаваемые docker'ом для которых необходимо копировать артефакты
    	docker_machines_w_artifacts = ['crawler', 'server_storm', 'server_web']
    }
    

    И используем их в проекте:
    // выполнить копирование шаблонов для docker с подстановкой значений
    docker_machines.each { machine ->
    	task "copyProvisionScripts_${machine}" (type: Copy, dependsOn: ['createStandDir']){
    ...
    	}
    }
    

  • Интеграция с репозитариями maven — больно громоздкое описание для включения в статью (и сама работа достаточно бесполезная с учётом наличия примеров в документации)

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

    Объявляем блоки:
    def javaTestLibrary(project) {
        project.dependencies {
    		testCompile "org.apache.commons:commons-lang3:${COMMONS_LANG_VER}"
    		testCompile "commons-io:commons-io:${COMMONS_IO_VER}"
    		testCompile "junit:junit:${TEST_JUNIT_VER}"
    		testCompile "org.mockito:mockito-core:${TEST_MOCKITO_VER}"
    		testCompile  "org.assertj:assertj-core:${TEST_ASSERTJ_VER}"
        }
    }
    

    И используем их в проекте:
    project(':token') {
    	javaProject(it)
    	javaLogLibrary(it)
    	javaTestLibrary(it)
    }
    

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

Спасибо за внимание!
Поделиться с друзьями
-->

Комментарии (2)


  1. apro
    01.08.2017 11:05

    Однако сейчас современные программы и подходы к разработке сильно изменились

    ИМХО конечно, но с точки зрения использования make ничего не поменялось.
    Все также нужно из множества файлов А получить множество файлов Б.


    Будь это получение xml отчета lint из множества .java и .xml. Или получение из множества файлов ресурсов с помощью aapt файла с .java кодом для доступа вида R.id.X,
    со всем этим gnu make отлично справиться.


    Другое дело, что язык для правил make слишком неудобный по сравнению например с groovy,
    да и встроенных правил (типа %.o : %.c) для сборки android проектов конечно нет.


    1. fedor_malyshkin
      01.08.2017 12:33

      Есть истинна в этих словах. Но сборка продукта системой сборки (а так же его тестирование и доставка) не всегда предполагают работу с только файлами — это могут быть и сетевые сервисы (FTP, HTTP), и сервисы ОС (docker например), и взаимодействие с облачными сервисами, а это становится для make не простой задачей (но не невыполнимой).

      Полагаю свой скрипт сборки с сохранением функционала я мог бы реализовать на ant, но зная про gradle — стоило бы?