TC Compiler Help - um apoio a fazer o build de bibliotecas
Esse posicionamento se refere a antes do início do totalcross-maven-plugin
que facilitou muitas coisas.
Muito do que o tc-compiler-help
se propõe a resolver já foi suplantado pelo totalcross-maven-plugin
,
mas muito não é tudo, né?
Como funciona o totalcross-maven-plugin
Esse plugin é o que há de mais esperto e o que eu gostaria de ter feito antes. Porém, não fiz, e também continuo não sabendo como se faz um plugin para o Maven.
Para gerar uma aplicação, o MOJO chamado desse plugin vai varrer todas as dependências. Mas não irá varrer
em vão, vai varrer procurando sinais de um tcz
dentro delas. Na arquitetura atual do totalcross-maven-plugin
,
para funcionar, em cima de uma dependência chamada abacate
se espera encontrar, na raiz do zip abacate.jar
,
o arquivo abacateLib.tcz
.
Se o tcz
existir, essa dependência é considerada uma dependência TotalCross, o tcz
é extraído de dento dela
e ainda é nomeado no all.pkg
. Tudo lindo e maravilhoso…
… SE a biblioteca em questão foi empacotada usando o totalcross-maven-plugin
. Em situações de bibliotecas
selvagens, normalmente elas não são empacotadas com esse plugin, portanto falta existir nelas o tcz
.
Isso normalmente não é um problema, pois o ecossistema TotalCross, apesar de ser baseado sobre o Java,
tem algumas idiossincrasias que o torna incompatíveis (ou, no mínimo, hostil) com diversas bibliotecas estrangeiras.
Mas, e em casos legados, em que a biblioteca foi gerada sem o .tcz
associado? Ou se simplesmente não for
auspicioso embarcar esse arquivo no .jar
gerado?
Como funciona o modelo “clássico” de dependências no TotalCross
Para gerar um .tcz
do jeito clássico, é necessário invocar a classe tc.Deploy
com algumas opções de linha de
comando para fazer essa geração. A priori, essa mesma classe gera também o executável, desde que seja fornecido
para ela uma das opções de plataforma; na inexistência de plataforma fornecida, só será criado um .tcz
de
biblioteca.
Assim, é possível especificar qual o .jar
que será compilado. Quando se está gerando aplicativos, é possível passar
o caminho para a classe principal ou mesmo para onde ficam os arquivos compilados Java, mas estamos aqui lidando com
bibliotecas, voltando à trilha principal.
Após gerar o .tcz
, se faz necessário colocar uma referência a esse arquivo no all.pkg
. Essa referência precisa seguir
o formato [L] abacateLib.tcz
, onde abacateLib
é a dependência a ser inserida. Antigamente, quando a instalação da TCVM
era separada da instalação do aplicativo, fazia sentido diferenciar dependências do tipo local da aplicação ([L]
) daquelas
que deveriam morar juntas à TCVM globalmente ([G]
), porém isso só é relevante atualmente para WinCE e WinMobile.
Mas, isso não é tudo. A TCVM tem uma limitação que, a partir de um .tcz
, só consegue carregar 4096 métodos distintos, 4096
atributos distintos e 4096 classes distintas. O que acontecia quando o alvo sendo tratado tinha mais do que a TCVM era capaz
de carregar? Bem, nos primórdios o build falhava miseravelmente mesmo e ficava a cargo do programador separar as preocupações
e dar seus pulos para ter bibliotecas com no máximo 4096 classes/métodos/atributos. Mas com o tempo foi adicionada a
capacidade do .tcz
sofrer um split automaticamente. Ainda no exemplo do abacateLib.tcz
, o primeiro split geraria
o arquivo abacateLib_1lib.tcz
. Sim, isso mesmo, com l
minúsculo.
De modo geral, a transformação é:
%.tcz
%_<n>lib.tcz
onde aqui <n>
é o número de vezes que foi disparado o split. Se for já voltado a uma biblioteca, temos o radical pós-fixo
o %Lib
.
Independente do split, ao adicionar a dependência no all.pkg
, o próprio tc.Deploy
vai atrás de pegar sozinho os splits.
Por exemplo, se o all.pkg
tivesse o seguinte conteúdo:
[L] abacateLib.tcz
[L] /path/to/marmotaLib.tcz
O tc.Deploy
irá procurar por abacateLib.tcz
no diretório atual e, também, por qualquer outro arquivo dentro do mesmo
diretório que satisfaça a regex abacateLib_[1-9][0-9]*lib\.tcz
. E irá procurar pelo marmotaLib.tcz
no diretório absoluto
/path/to/
de modo semelhante, portanto resgatando os arquivos /path/to/marmotaLib.tcz
e os que satisfaçam a regex
/path/to/marmotaLib_[1-9][0-9]*lib\.tcz
.
Diferenças entre o jeito clássico e o totalcross-maven-plugin
O plugin, na hora de criar os .tcz
, delega ao tc.Deploy
o trabalho duro. Porém, ele ignora a existência do split de .tcz
s.
Tanto ao empacotar como ao extrair. Então, temos um problema a ser tratado. Quem estiver se sentindo aventureiro, o repositório é
https://github.com/TotalCross/totalcross-maven-plugin/.
Porém, ele tem uma vantagem indiscutível em relação ao clássico: gerência do all.pkg
. O plugin cria automaticamente o all.pkg
na inexistência dele, e também aparentemente mantém uma boa relação com o all.pkg
pré-existente.
O tc-compiler-help
como alternativa
Na criação desse auxiliar da compilação de Java para formato TotalCross, tive de lidar com problemas distintos do que aqueles
que o plugin veio resolver. Em primeiro lugar, o .jar
já estava formado. Eu não poderia mexer nele. E também (originalmente)
muitas das bibliotecas sofriam split.
Então, eu precisava de uma maneira alternativa para conseguir gerar os .tcz
s. Inicialmente, quando existiam poucas dependências,
era feito na mão o processo, via um script configurado no build do Jenkins chamando o tc.Deploy
de um lugar pré-instalado. Só
que isso implicava uma boa quantidade de “duplicação” de código, da chamada dessa classe em um script. E também a manutenção do
all.pkg
dentro do repositório. Quando saímos de 3 dependências para 4, já foi começado a tomar outro rumo.
No lugar de definir externamente o que compilar, trouxemos para dentro de uma classe Java o que compilar. Dado um predicado arbitrário,
passando por todos os .jar
do classpath, julgar se precisa compilar baseado apenas no nome do arquivo. Algo que tem em comum é
que eles tem no groupId
(e, portanto, no esquema de diretórios) o nome softsite
, e posso excluir o tc-compiler-help
como parte
do predicado. Pronto, isso me permite trabalhar com o legado dos antigos .jar
sem maiores estresses.
Assim sendo, o tc-compiler-help
ficava encarregado de fazer algumas coisas:
- percorrer o classpath perguntando se o usuário gostaria de usar aquele
.jar
- caso sim, verificar se tinha algum
.tcz
associado - caso não, ou caso o
.tcz
seja mais antigo do que o.jar
, gerar um novo.tcz
- adicionar na lista do
all.pkg
o caminho para o.tcz
- chamar o
tc.Deploy
para gerar a aplicação - restaurar o estado anterior do
all.pkg
Pronto, só isso. Note que, na época, o maior suporte que o TotalCross fornecia era para o Java 8, então peguei o classloader padrão
do Java (que por sinal era instância de URLClassLoader
) e isso funcionava bem. Porém, com o advento do jigsaw e módulos do Java 9,
isso não é mais uma simples verdade e, portanto, o tc-compiler-help
não funciona adequadamente.
O .tcz
foi convencionado para ser gerado na mesma pasta do .jar
(que, por sua vez, está dentro do MAVEN_HOME
).
Caveats
Antes de lidar aqui com o exemplo, mostrar algumas das limitações do tc-compiler-help
.
A primeira é a já citada necessidade de rodar com Java 8. Não apenas limitado a ser compilado com target para Java 8, preciso estar rodando em cima de Java 8.
Outra é que ele escreve no diretório onde fica a dependência Maven. Como está lá, isso pressupõe que o usuário que estiver rodando
o comando tenha permissão para adicionar coisas dentro do MAVEN_HOME
(o que talvez não seja verdade).
Tem uma mais sutil. No caso, ela é relativa ao modo como o predicado é fornecido. O que o predicado vai receber é uma string com o caminho
absoluto do nome do arquivo. Se o predicado fizer apenas uma verificação de substring (por exemplo, abacate
), e o usuário que estiver
rodando o comando tiver essa substring no nome, e o comando estiver sendo executado dentro da HOME
do usuário, então todo .jar
será
considerado como válido. Isso pode ser relativo não só ao abacate
dependência com o abacate
usuário, mas talvez o nome do projeto
seja assim e na criação do CI o predicado dar falso positivo para todo mundo.
Para poder pegar todo o classpath das dependências, se faz aconselhável rodar pelo Maven (mojo exec:java
). Precisa ser executado
após a geração do .jar
.
Por fim, quando detecta que precisa gerar um novo .tcz
, ele não irá imediatamente remover os demais .tcz
s gerados pelo split
anterior. Então, se por acaso a versão anterior de abacate
gerasse dois splits e a nova gerasse apenas um único split, o arquivo
abacateLib_2lib.tcz
iria vazar e ser instalado junto da aplicação, com resultados imprevisiveís.
Usando na prática
Vamos pegar um projeto de exemplo aqui, o stream-support-totalcross-sample
.
Esse exemplo foi criado só para demonstrar a possibilidade de usar algo
extremamente semelhante ao java.util.stream.Stream
do Java 8 dentro do TotalCross,
através do projeto totalcross-functional-toolbox
.
O projeto em si é bem simples, contém 3 classes apenas:
- a classe
App
que estende aMainWindow
- a classe
SampleApp
que apenas chamaTotalCrossApplication.run
para a classe principal - a classe
CompileApp
que configura oCompilationBuilder
e lida com parâmetros CLI
Vamos focar na configuração principal do CompilationBuilder
. As primeiras coisas que se podem
ver são que estamos configurando variáveis relativas ao TotalCross:
- a chave usada do TotalCross
- onde está TotalCross
Em seguida, começamos a ver coisas relativas à construção propriamente dita, quando
se informa que se deseja compilar para WIN32. O próximo passo é informando o predicado
de compilação da dependência. Esse projeto em específico foi feito para mostrar usando
Stream
no TotalCross, então foi necessário colocar lá algo que conseguisse colocar identificar
corretamente a dependência do totalcross-functional-toolbox
.
Depois, finalmente, temos a classe da MainWindow
, informação de que se trata de um
“single package” e o comando para se fazer o .build()
.
Chamando pela linha de comando pelo Maven, seria algo assim:
./mvnw clean package exec:java -Dexec.mainClass="br.com.softsite.streamsupport.CompileApp" \
-Dexec.args="-n \
-P retrolambda
Note que, nesse caso específico, eu só quero fazer o dry-run para verificar se tudo está compilando adequadamente.