A alguns dias atrás eu li este twitt do Martin Fowler: “you don’t want a build tool which automatically downloads unresolved dependencies before cleaning out yr build output: http://bit.ly/59Rl85“, li todo o post e ele fala de forma bastante prolixa de alguns dos motivos que me fazem não gostar do Maven.
Não me levem a mal, eu já tentei utilizar ele algumas vezes, mas eu não consigo gostar de uma ferramenta que acha que sabe mais do meu projeto do que eu mesmo (ou o cliente, ou os desenvolvedores, …).
Ou pior que isto, uma ferramenta que tem a infeliz mania de tentar fazer um backup da internet antes de cada build só para verificar se tem a última versão das dependências disponível …
Como é citado no post, não acho que alguma ferramenta vá saber exatamente o que é necessário para qualquer projeto, até por que cada projeto é um projeto, e cada projeto tem suas peculiaridades, e eu simplesmente desisti todas as vezes que precisei configurar alguma destas peculiaridades no maven e voltei para o ANT.
O ANT é uma ferramenta bastante flexível, e pelo que eu tenho visto no mercado, fora alguns teimosos que preferem usar o maven mesmo passando muito mais trabalho do que o necessário, o ANT é o “defacto standard” para builds em Java, mas algumas vezes a “linguagem de script” do ANT dificulta as coisas quando se precisa realmente de um script para fazer alguma coisa durante o build, então resolvi usar Ruby para escrever os builds, ou seja, utilizar uma linguagem de scripts de verdade.
Ai pensei, como é que vou fazer para compilar meu projeto java utilizando o Rake? A linguagem de script é muito fodastica, é Ruby, eu me sinto bem programando em Ruby, mas e como compilar?
Fui perguntar ao oraculo e descobri o BuildR e o Raven que fora o fato de não utilizarem XML e sim Ruby, conseguem repetir todos os erros do Maven, eles parecem “ports do Maven para o Rake” e eu não sei por que alguem iria fazer isto, se você gosta tanto assim do Maven, use ele mesmo …
Mas do Rake eu gosto, me acostumei com ele trabalhando com o Rails, é muito fácil de automatizar tarefas relacionadas a um projeto utilizando o Rake, e não apenas o “build”, mas algumas tarefas que as vezes precisam ser automatizadas, como um merge freqüente com algum sub projeto desenvolvido em outra parte do mundo …
Isto me criou apenas um problema, como compilar, empacotar, …
Ou seja, me faziam falta as tasks básicas do ANT que eu utilizo sempre. As outras tarefas são melhor executadas na minha opinião pelo próprio Rake ou até mesmo por um script em Ruby, mas estas tarefas básicas iriam fazer falta, e para resolver isto eu criei uma classe wrapper para os comandos do JDK, que pode ser estendida depois, não é algo 100% rake, mas eu achei que ficou legal assim, se alguem não concordar e tiver idéias para melhorar estou aceitando sugestões
O wraper para os comandos do JDK ficou assim:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | class JavaUtil RAW_COMMANDS = %w{appletviewer apt extcheck idlj jar jarsigner java javac javadoc javah javap javaw javaws jconsole jdb jhat jinfo jmap jps jrunscript jstack jstat jstatd jvisualvm keytool kinit klist ktab native2ascii orbd pack200 packager policytool rmic rmid rmiregistry schemagen serialver servertool tnameserv unpack200 wsgen wsimport xjc} def initialize(jdk_home=nil) @commands = {} @jdk_home = jdk_home || ENV['java_home'] @default_for_command = {} @global_default = {} init_commands end def method_missing(met,*args) if RAW_COMMANDS.include? met.to_s execute_command met, *args else super.method_missing met, *args end end def respond_to?(met) RAW_COMMANDS.include?(met.to_s) || super.respond_to?(met) end def default_parameter(param,value) @global_default[param] = value end def default_parameter_for(met,param,value) params = @default_for_command[met] || {} params[param] = value @default_for_command[met] = params end private def init_commands @jdk_bin = File.join @jdk_home , "bin" RAW_COMMANDS.each do |cmd| @commands[cmd.to_sym] = File.join @jdk_bin, cmd end end def update_or_concat_with_defaults(opts,defaults) defaults.each do |key,value| param = opts[key] if !param param = value else if param.is_a? Array param << value param.flatten! end end opts[key] = param end end def execute_command(cmd, *args) actual_command = @commands[cmd.to_sym] if args opts = {} opts.update args.pop if args.last.is_a? Hash update_or_concat_with_defaults opts, @global_default update_or_concat_with_defaults opts, @default_for_command[cmd.to_sym] if @default_for_command[cmd.to_sym] opts.each do |key, value| param = value param = param.join File::PATH_SEPARATOR if param.is_a? Array actual_command << " -" << key.to_s << " " << param end actual_command = "#{actual_command} #{args.join ' '} " end puts actual_command res = %x{#{actual_command}} puts res [$?,res] end end |
A minha idéia dos parâmetros default globais tem um pequeno problema, alguns comandos não recebem os mesmos parâmetros, mas é possível setar parâmetros padrão por comando, o que ficou legal, e deixou a compilação mais limpa …
A classe pode ser utilizada com qualquer JDK, inclusive instâncias diferentes podem utilizar JDKs diferentes para o mesmo build, basta passar o “JAVA_HOME” no construtor, por padrão a variável de ambiente é utilizada …
Mas beleza, como é que eu utilizo esta tranqueira em um Rakefile agora? bom, o meu Rakefile para o projeto de exemplo ficou assim:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | require 'lib/java_util' @java_util = JavaUtil.new task :default => :test SRC_FILES = FileList.new 'src/**/*.java' TST_FILES = FileList.new 'test/**/*.java' CLASSPATH = FileList.new "#{File.join(ENV['TOMCAT_DIR'], 'lib').gsub /\\/,'/'}/*.jar" @java_util.default_parameter_for :java, :classpath, CLASSPATH.to_a @java_util.default_parameter_for :javac, :classpath, CLASSPATH.to_a directory 'output/classes' directory 'output/tests' desc "Compile all the java files" task :compile => ['output/classes','output/tests'] do @java_util.javac SRC_FILES, :d => 'output/classes' @java_util.javac TST_FILES, :d => 'output/tests', :classpath => ['output/classes',"#{ENV['JUNIT_DIR']}\\junit-4.4.jar"] end desc "Creates the package after compilation" task :package => :compile do @java_util.jar '-cf output/target.jar -C output/classes .' cp 'output/target.jar', 'WebContent/WEB-INF/lib' @java_util.jar '-cf output/target.war -C WebContent .' end desc "Runs the tests after packaging" task :test => :package do test_classes = FileList.new 'output/tests/**/*.class' test_classes.gsub! /output\/tests\/(.*)\.class/,'\1' test_classes.gsub! /\//, '.' @java_util.java "org.junit.runner.JUnitCore #{test_classes.join ' '}", :classpath => ['output/tests','output/target.jar',"#{ENV['JUNIT_DIR']}\\junit-4.4.jar"] end desc "Clean up all the mess we created" task :clean do rm_f 'output' rm_t 'WebContent/WEB-INF/lib/target.jar' end |
O código dos testes não precisava ser tão complexo, eu poderia ter criado um wrapper para ele, a mesma coisa para a criação do jar, poderia até mesmo ter utilizado o “rubyzip” para deixar mais bonitinho, mas a idéia por enquanto é ser bem simples.
Estou utilizando este build em um projeto, se engrenar provavelmente a biblioteca vá crescendo, mas acho que por agora já serve para começar a brincar e ver o que vocês acham da idéia.
A classe “JavaUtil” precisa ser mais testável, mas isto tornou ela complexa demais para o exemplo deste post, se eu convencer o resto da equipe a continuar usando esta solução vou melhorando ela aos poucos
Acho que vou separar a montagem do comando e a execução do mesmo, ou transformar cada comando em uma classe para facilitar a expansão da biblioteca e tornar mais testável, ou até mesmo as duas coisas.
No momento a classe não é nada testável, mas já esta divertida e o meu bluid diminuiu muitas linhas depois que eu converti ele de ANT para Rake utilizando esta lib
PS.: quem quiser pegar o projeto de testes para brincar, só para olhar ou até mesmo para implementar algumas melhorias, ele esta publicado no github. Se implementarem alguma melhoria, não esqueçam de enviar um pull request para que eu possa fazer o merge das alterações