29 de maio de 2013

Verificando a versão de bibliotecas e programas em Delphi

Um dos aspectos mais complicados de se gerenciar na entrega (deploy) de uma nova release de um programa é garantir a compatibilidade entre as bibliotecas DLL e outros executáveis que compõem a solução. Isso é particularmente verdade se esses arquivos também podem ser entregues de forma independente e/ou em pastas separadas, como em geral ocorre com os pacotes BPLs que compõem soluções em Delphi ou C++ Builder.

A ABC71 adota uma estratégia interessante para minimizar esse tipo de problema. Quando construímos aplicações Windows, sejam programas ou bibliotecas, podemos adicionar ao arquivo gerado informações extras úteis: os recursos. As informações incluídas como recursos num executável podem ser facilmente recuperadas via programação, do mesmo modo que o Windows Explorer faz quando apresenta a guia Versão nas propriedades de um arquivo.

O ERP comercializado pela ABC71 é composto por dezenas de bibliotecas e pacotes independentes, contendo as diversas funcionalidades do sistema. A ideia é incluir um número de versão em todos os executáveis, através de recursos do Windows. Como cada arquivo é carregado dinamicamente conforme a necessidade, podemos utilizar o momento da carga para verificar se a versão do arquivo é compatível com o programa e, em caso negativo, notificar o usuário para que ele atualize os arquivos. Esse mecanismo pode ser aplicado também a bibliotecas de terceiros, como o ADO ou pacotes de componentes visuais.

Para implementar uma verificação nesses moldes em suas próprias bibliotecas, o primeiro passo é adicionar a elas um arquivo de recursos do Windows com informações relevantes sobre a versão atual. O quadro a seguir mostra um trecho do arquivo RC que adicionamos a nossos projetos:
#define DATARELEASE "28/05/2013\0"
#define VERSAO "26.9.5.6\0"
#define VERSAO_DB "9.5\0"

#ifdef WCOMP1
#define DESCRICAO "Componentes auxiliares\0"
#define NOME_INTERNO "WComp1\0"
#else
#define DESCRICAO "ERP Omega\0"
#define NOME_INTERNO "Omega\0"
#endif

VS_VERSION_INFO VERSIONINFO
FILEVERSION 26,9,5,6
PRODUCTVERSION 26,9,5,6
FILEFLAGSMASK 0x3fL
FILEFLAGS 0x8L
FILEOS 0x4L
FILETYPE 0x1L
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0"
BEGIN
VALUE "Comments",DESCRICAO
VALUE "CompanyName","ABC71 Soluções em Informática\0"
VALUE "InternalName", NOME_INTERNO
VALUE "LegalCopyright", "2013 \xA9 ABC71 Soluções em Informática\0"
VALUE "ProductName", "ERP OMEGA\0"
VALUE "ProductVersion", VERSAO
VALUE "FileDescription", "ERP OMEGA\0"
VALUE "FileVersion", VERSAO
VALUE "VersaoBaseDados", VERSAO_DB
VALUE "DataRelease", DATARELEASE
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END
O exemplo usa compilação condicional para diferenciar os diversos projetos, permitindo reaproveitar o mesmo arquivo em todos eles. Isso elimina a necessidade de alterar cada um deles quando a versão (ou outra informação) muda; basta recompilá-los com a versão nova do arquivo de recursos. Note que a sintaxe do arquivo lembra mais a do C/C++ do que a do Pascal/Delphi, incluindo os IFDEF e o terminador nulo ao fim de cada texto fixo.

Uma vez incluídas essas informações em cada executável, pacote e biblioteca, o passo seguinte é extrai-las para comparar com as existentes no programa que está carregando o pacote/biblioteca. A API do Windows possui uma série de funções para essa tarefa, agrupadas sob o nome de Version Information Functions. O quadro abaixo mostra uma função simples usando essa API; ela é capaz de recuperar qualquer uma das informações contidas no arquivo de recursos.
function GetStringValue (AHandle: HMODULE; ATexto: String) : String;
var lIgnore, lTamInfo: DWORD;
lBuffer: LPVOID;
lInfo: PChar;
lPath, lTrans: String;
lModulo: array[0..MAX_PATH] of Char;
begin
{ Obtem o nome do executável, DLL ou pacote cujo handle foi informado no parâmetro }
GetModuleFileName (AHandle, lModulo, sizeof(lModulo));

{ Calcula o tamanho que deve ter o buffer para recuperar as informações de versão }
lTamInfo := GetFileVersionInfoSize(lModulo, lIgnore);
GetMem (lBuffer, lTamInfo);

{ Recupera as informações no buffer alocado }
if GetFileVersionInfo(lModulo, lIgnore, lTamInfo, lBuffer)
then begin
{ Lingua 0409 com code page 04B0. No arquivo de recurso desse exemplo existe apenas textos nessa lingua.}
lTrans := '040904B0';

{ Monta o caminho onde está o texto procurado no buffer }
lPath := '\StringFileInfo\' + lTrans + '\' + ATexto;

{ Recupera o valor do texto solicitado }
if VerQueryValue (lBuffer, pChar(lPath), Pointer (lInfo), lTamInfo)
then
Result := lInfo
else
Result := '';
end;
FreeMem(lBuffer);
end;
Pelo código no quadro, observamos três etapas distintas para obter o valor desejado. Primeiro, a função GetFileVersionInfoSize nos reporta o tamanho do bloco de informações de versão contido no módulo (executável, pacote ou DLL). Com essa informação, podemos alocar a memória necessária para ler o bloco todo.

Na segunda estapa, a função GetFileVersionInfo extrai o bloco de informações e o coloca na memória que alocamos anteriormente. Finalmente, o programa busca o texto solicitado montando um caminho e chamando a função VerQueryValue. Um arquivo de recursos pode ser organizado com blocos de textos em línguas específicas (português, inglês, etc.) de modo que o conteúdo apropriado pode ser exibido de acordo com as preferências do usuário; por questão de simplicidade, esse exemplo trata apenas uma língua, motivo pelo qual foi possível manter a busca fixa, sem me preocupar em levantar as línguas incluídas.

Com essa função, podemos obter, por exemplo, a data de release do módulo usando o seguinte código:
var data: String;
begin
{ Obtem a data de release contida no programa atual }
data := GetStringValue (0, 'DataRelease');
Para implementar a verificação de compatibilidade de versão, podemos incluir código similar ao anterior na área de initialization numa unit de um pacote:
var dataP, dataE : String;

initialization

dataE := GetStringValue (0, 'DataRelease');
dataP := GetStringValue (HInstance, 'DataRelease');

if (dataE <> dataP) then begin
ShowMessage ('Este pacote é incompatível com o executável.'#13#10 +
' Data do Pacote : ' + dataP + #13#10 +
' Data do Executável : ' + dataE );
ExitProcess(-1);
end;
end.
Com isso, quando nosso programa carregar esse pacote, o código acima obterá o campo DataRelease tanto do executável quanto do próprio pacote e, se não forem compatíveis, o programa é abortado. O exemplo é bastante simples mas a solução pode ser usada para contemplar informações mais complexas, como um número mínimo de versão ou release. Essa técnica também é aplicável às DLLs.

Há um artigo neste endereço do site delphiDabbler dando explicações mais detalhadas sobre o funcionamento das APIs de versão do Windows. Ele também constrói uma classe para encapsular a leitura dessas informações, facilitando bastante o processo de extrair aquelas que interessam para a validação de compatibilidade.

Nenhum comentário :

Postar um comentário

OBS: Os comentários enviados a este Blog são submetidos a moderação. Por isso, eles serão publicados somente após aprovação.

Observação: somente um membro deste blog pode postar um comentário.