Знакомимся с Ant

Jan 12, 2013 18:09 · 633 words · 3 minute read tutorial

Понадобилось мне установить программку, которая вроде как и кросплатформенная, однако инсталлятор написан на дикой смеси bat'ников, xml и xsl преобразованиях с помощью msxsl.exe — кошмар для линуксоида. А так как это далеко не разовая операция, то было принято решение написать свой инсталлятор с кофе и муравьями 🙂 О проблемах и их решениях прошу под кат.

Задача вроде как сводится к написанию xml для ant, который будет копировать куда надо файлики, выполнять xsl-преобразования, в общем, повторять все действия, которые указаны в его предыдущей реинкарнации. Прочитав небольшой мануал, научился копировать файлы и задавать цели — ничего сложного. Впереди — xslt! Движки xalan и xslt между собой хоть и отличаются, но не значительно. В ant xslt задаётся следующей командой:

<xslt in="from.xml" out="to.xml" style="with.xsl" classpathref="java-classpath"/>

где java-classpath должен указывать на нужные jar файлы:

<path id="java-classpath">
    <fileset dir="Source/ant/bin/lib">
        <include name="*.jar"/>
    </fileset>
</path>

Нужными в данном случае являются xalan.jar, serializer.jar, bsf.jar, commons-logging.jar. Отдельно следует упомянуть js.jar, который даёт возможность задавать js-функции внутри xsl (об этом ниже). Подключив нужный namespace в xml строкой xmlns:xalan = "http://xml.apache.org/xalan". В xsl:stylesheet, мы можем воспользоваться нужными функциями, например, nodeset (кстати, в msxsl она называется node-set). Работа с js с помощью xalan и msxsl тоже отличается, вот пример для xalan:

<xalan:component prefix="user" functions="trim">
    <xalan:script lang="javascript">
        //Удаляет лишние пробельные символы вначале и в конце строки
        function trim(string) {
            return string.replace(/(^\s+)|(\s+$)/m, "");
        }
    </xalan:script>
</xalan:component>

Итак, с xsl вроде бы разобрались. Следующая вкусняшка — пакетная обработка файлов. В ant для этого есть команда apply. Пример использования:

<apply executable="wrap" dir="${distrib}/schema" parallel="true" verbose="true">
    <arg value="iname="/>
    <srcfile/>
    <arg value="oname="/>
    <targetfile/>
    <arg value="edebug=wrap_new_sql"/>
    <fileset dir="${distrib}/schema">
        <include name="*.sql"/>
    </fileset>
    <mapper type="glob" from="*.sql" to="../../${distrib}/schema/*_wrap.sql"/>
</apply>

Здесь мы обрабатываем утилитой wrap все файлы из fileset, переименовав их с помощью mapper. Далее мне потребовалось запустить несколько sql скрипов с помощью sqlplus. И вот чёрт меня дёрнул набрать в гугле «ant oracle» — третья ссылка на некоторое расширение (incanto) ant, которое выполняет то, что нужно. Ага, подумал я, так здесь и расширения можно писать, но об этом позже. Для incanto достаточно бросить его к либам анта, подключить namespace xmlns:ora = "antlib:net.sf.incanto" и выполнить таск:

<ora:sqlplus logon="${user}/${password}@${db}" dir="${distrib}/schema_user" start="setup.sql"/>

Красота! И тут у меня возникла мысль написать своё расширение, т.к. требовалось выполнить некоторое действие над группой файлов (к сожалению, apply тут мне бы не помог). Это оказалось сделать на удивление просто. Допустим, нам нужно вывести в лог все файлы в определённом каталоге, который будет передаваться через параметр dir. Для этого создадим пакет, в котором будет описатель (antlib.xml) и рабочий класс (Out.java). Пример antlib.xml:

<?xml version="1.0"?>
<antlib>
    <taskdef name="out" classname="com.tyvik.installer.Out"/>
</antlib>

Думаю, в объяснении не нуждается — просто определили таск out. Сам класс:

package com.tyvik.installer;

import java.io.File;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;

public class Out extends Task {
    private File inDir;

    public Out() {
        super();
    }

    public void setDir(File directory) {
        this.inDir = directory;
    }

    public void execute() {
        log(String.format("Out from \"%s\"", this.inDir.getPath()));
        if (!this.inDir.isDirectory()) {
            throw new BuildException(String.format("\"%s\" must be directory!", this.inDir.getPath()));
        } else { // нужный код
        }
    }
}

Всё тривиально — наследуемся от Task, запоминаем параметр с помощью setDir, если это не каталог, то бросаем BuildException. Вывод в лог производится процедурой log. Использовать это можно также, как и incanto — подключить namespace и вызвав таск:

<project default="all" xmlns:tyvik="antlib:com.tyvik.installer">
    <target name="all">
        <ora:out dir="c:\Users"/>
    </target>
</project>

В общем, муравей оставил приятное впечатление благодаря своей универсальности и продуманности. Скорее всего я раскрыл далеко не все возможности муравья, посему просьба не стесняться, а задавать вопросы в комментариях :) Мораль сей басни такова: если приложение задумывается как кросплатформенное, делайте его целиком кросплатформенным. Удел батников — максимум небольшие скрипты с тривиальными командами, скриптов на ps — администрирование Windows и работа с .Net. Используйте инструменты по назначению.

P.S. Хм, видимо, в старом ant (до 1.7) вызов внешних обработчиков производится не через namespace, а через определение целей в самом build.xml:

<taskdef name="out" classname="com.tyvik.installer.Out">
    <classpath>
        <pathelement location="./installer.jar"/>
    </classpath>
</taskdef>

P.P.S. Выложил код на Github.