1
votes

My Delphi application uses the Microsoft Resource Compiler (rc.exe) to compile a list of strings (text format in a .rc file) like this:

Language LANG_KOREAN, SUBLANG_KOREAN
STRINGTABLE
BEGIN
    cszLanguageName "Korean"
    <etc>
END

into a .res file. All tables contains the same IDs (such as cszLanguageName). I maintain two separate resource string files. One contains mostly European languages (English, Czech, etc) which I call "Standard.rc". The other is called "Alternate.rc" and contains all other languages like Korean, Thai, etc.

A compile-time switch determines which file is linked into the EXE:

{$IFDEF ALT_LANG}
    {$R 'source\Alternate.res'}
{$ELSE}
    {$R 'source\Standard.res'}
{$ENDIF}

That was background, now to the question!

Is it possible to determine, given the path to an EXE and using something like the Windows GetFileVersionInfo method, which STRINGTABLE resources are in the EXE? If it were possible to determine whether:

Language LANG_KOREAN, SUBLANG_KOREAN

or

Language LANG_CZECH, SUBLANG_DEFAULT

was included, then the EXE could be identified as "standard" or "alternate".

Currently, without actually executing the EXE, the only difference is size in bytes which is an unreliable heuristic. C++ or C# is fine. I can adapt to Delphi or write a separate utility which is called from Delphi.

UPDATE

Based on the comment from LU RD, I created the following Version.rc file:

// Version information resource file
VS_VERSION_INFO VERSIONINFO
BEGIN
    BLOCK "StringFileInfo"
    BEGIN
        BLOCK "080904b0"
        BEGIN
        VALUE "InternalName", "Standard"
        END
    END
END

This was then compiled (with the Microsoft Resource Compiler) into Version.res and linked into the application using {$R 'source\Version.res'}. This compiles as expected, but when I try to read the InternalName field from the EXE it is blank. If I manually set InternalName in the project properties to "Test", then it is set out as "Test". What am I doing wrong and how do I overwrite what has been manually entered in the project properties?

1
You can change version number at build time and have different info for a certain language. Later it is easy to detect the status with a call to GetFileVersionInfo.LU RD
You could use a resource editor, but perhaps a better idea is to add VERSIONINFO fields into the resource files themselves (rather than letting the compiler update them with 'include version info in project', then right clicking the exe and going to the properties tab should show the information.Dsm
@Dsm: I'm looking for the information at runtime. The version number is the same for both "standard" and "alternate" EXEs. Apart from the STRINGTABLE resources linked into the EXE, everything else is the same.AlainD
@AlainD The VERSIONINFO block contains many fields, not just the version number, and you can, I believe, add your own if the ones you need are not there. It is just that these properties are the ones you see on the properties tab when you right click. You say everything is the same - but that is a happenstance not a requirement, and you can add whatever you want (more or less) into the resource file. You say you need the information at run time. That is at odds with your question.Dsm
@Dsm: Ok, thanks. I'm looking into the suggestion from LU RD about adding customised version information into the EXE at compile-time (and then extracting that information at run-time).AlainD

1 Answers

1
votes

NOTE: This is not an answer to the question of how to determine whether a specific STRINGTABLE resource is included in the final EXE, but an alternate method to resolve the problem posed.

This question is partially based on this answer.

Step 1: Enter your project properties and the Version Info tab. If version information (such as version numbers or CompanyName) have been set, copy down all the relevant information. Now turn off Include version information in project.

Step 2: Create a Version.rc and Version_Alt.rc file. Your exact version may differ, but this is a good starting template:

// Version information resource file
#include "windows.h"
#include "VersionCommon.txt"

1 VERSIONINFO
FILEVERSION VER_NUM_MAJOR, VER_NUM_MINOR, VER_NUM_RELEASE, VER_NUM_BUILD
FILEOS VER_FILEOS
FILETYPE VFT_APP
{
    BLOCK "VarFileInfo"
    {
        VALUE "Translation", TRANSLATION_LANG_ID, TRANSLATION_CHARSET
    }

    BLOCK "StringFileInfo"
    {
        // Note: The block address is a concatenation of the locale ID and code page. This would be "040904E4" for English (US).
        BLOCK "080904E4"
        {
            VALUE "CompanyName",        STRING_COMPANY_NAME
            VALUE "FileDescription",    STRING_FILE_DESCRIPTION
            VALUE "InternalName",       STRING_INTERNAL_NAME_STD
            VALUE "LegalCopyright",     STRING_LEGAL_COPYRIGHT
            VALUE "LegalTrademarks",    STRING_LEGAL_TRADEMARKS
            VALUE "ProductName",        STRING_PRODUCT_NAME
            VALUE "ProductVersion",     STRING_PRODUCT_VERSION
            VALUE "Comments",           STRING_COMMENTS
            VALUE "Homepage",           STRING_HOME_PAGE
        }
    }
}

This is for the Version.rc file. InternalName will be STRING_INTERNAL_NAME_ALT for Version_Alt.rc, otherwise everything else is the same.

Step 3: Create VersionCommon.txt which might look like this:

// Common version information (between standard and alternate language)

// Version info
#define VER_NUM_MAJOR       1
#define VER_NUM_MINOR       2
#define VER_NUM_RELEASE     3
#define VER_NUM_BUILD       4
#define VER_FILEOS          0x00000004L     // 32-bit Windows

// Translation info
#define TRANSLATION_LANG_ID     0x0809      // Locale: English (UK)
#define TRANSLATION_CHARSET     0x04E4      // Code Page: 1252

// String file info
#define STRING_COMPANY_NAME         "YOUR-COMPANY\0"
#define STRING_FILE_DESCRIPTION     "Software to do amazing things\0"
#define STRING_INTERNAL_NAME_STD    "Standard\0"    // ALT_LANG not defined
#define STRING_INTERNAL_NAME_ALT    "Alternate\0"   // ALT_LANG is defined
#define STRING_LEGAL_COPYRIGHT      "Copyright (C) YOUR-COMPANY\0"
#define STRING_LEGAL_TRADEMARKS     "LEGALISE STATEMENT?\0"
#define STRING_PRODUCT_NAME         "Groovy\0"
#define STRING_PRODUCT_VERSION      "SOME-VERSION-INFO\0"
#define STRING_COMMENTS             "SOME-COMMENTS\0"
#define STRING_HOME_PAGE            "OPTIONAL-YOUR-WEBSITE\0"

Step 4: Write a batch file that compiles your resource scripts. Note that later versions of Delphi can be configured to compile the resources for you. The batch file might look like this:

@echo off
rem Version.rc and Version_Alt.rc are used to create version information that is linked into
rem the main Delphi application. "InternalName" is used to indicate whether ALT_LANG is defined.

echo Setting the program path (change this if your path is different)
set SOURCE_PATH=<PATH-TO-FOLDER-CONTAINING-RC-FILES>

echo .
echo Use Visual Studio tools to generate the version .RES files
@echo on
cd <PATH-TO-VISUAL-STUDIO-BIN-FOLDER-CONTAINING-RC.EXE>
call vcvars32
rc /r %SOURCE_PATH%\Version.rc
rc /r %SOURCE_PATH%\Version_Alt.rc
echo .
@echo off
echo .

rem pause <- uncomment this to debug errors
exit

Step 5: Open your Delphi project (.dpr) in a text editor and link these .RES files into the final EXE:

{$IFDEF ALT_LANG}
    {$R 'source\Strings_Alt.res'}
    {$R 'source\Version\Version_Alt.res'}
{$ELSE}
    {$R 'source\Strings.res'}
    {$R 'source\Version\Version.res'}
{$ENDIF}

Step 6: You've now got version information included in your file and just have to read InternalName (which will be "Standard" or "Alternate"). This can be done as follows:

strLanguage := GetSpecificFileVersionInfo('YOUR-EXE.exe', 'InternalName');

and the code for GetSpecificFileVersionInfo is:

function GetSpecificFileVersionInfo(szFile: PChar; strInfo: String) : String;
var
    pstrBuffer: PChar;
    dwSize, dwLength: DWORD;
    pVersion: pointer;
    strKey: String;
begin
    // Return the specified file version information
    // Adapted from: http://www.swissdelphicenter.com/en/showcode.php?id=1047

    // Typical values in the VERSIONINFO resource of the application include:
    // * CompanyName
    // * FileDescription
    // * InternalName
    // * LegalCopyright
    // * LegalTrademarks
    // * ProductName
    // * ProductVersion
    // * Comments

    // Additional fields may be available if the version information has been updated
    Result := '';
    dwSize := GetFileVersionInfoSize(szFile, dwSize);
    if (dwSize > 0) then
        begin
        pstrBuffer := AllocMem(dwSize);
        try
            if (    (GetFileVersionInfo(szFile, 0, dwSize, pstrBuffer)) and
                    (VerQueryValue(pstrBuffer, '\VarFileInfo\Translation', pVersion, dwLength))) then
                begin
                strKey := Format('\StringFileInfo\%.4x%.4x\%s', [
                    LoWord(Integer(pVersion^)),
                    HiWord(Integer(pVersion^)), strInfo]);
                if (VerQueryValue(pstrBuffer, PChar(strKey), pVersion, dwLength)) then
                    Result := StrPas(pVersion);
                end;
        finally
            FreeMem(pstrBuffer, dwSize);
        end;
        end;
end;

Job done. Now you've solved two problems. The first is your original one which is to identify how the EXE was compiled (without actually running the EXE). The second is one you probably didn't realise which is your manually entered version information was error-prone...now you have included more automated version information.