Created
September 21, 2018 15:40
-
-
Save cas--/5168013c49a87ce848086a1b0d3c770e to your computer and use it in GitHub Desktop.
Stamp windows exe files with version information
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" Stamp a Win32 binary with version information. | |
Original code from pywin32 module. | |
Modified to only use ctypes, making it standalone. | |
""" | |
import ctypes | |
import os | |
import struct | |
import glob | |
import optparse | |
BeginUpdateResource = ctypes.windll.kernel32.BeginUpdateResourceA | |
EndUpdateResource = ctypes.windll.kernel32.EndUpdateResourceA | |
UpdateResource = ctypes.windll.kernel32.UpdateResourceA | |
VS_FFI_SIGNATURE = -17890115 # 0xFEEF04BD | |
VS_FFI_STRUCVERSION = 0x00010000 | |
VS_FFI_FILEFLAGSMASK = 0x0000003f | |
VOS_NT_WINDOWS32 = 0x00040004 | |
def MAKELANGID(p, s): | |
return DWORD( (s << 10) | p ) | |
LANG_NEUTRAL = 0x00 | |
SUBLANG_DEFAULT = 0x01 # user default | |
null_byte = "\0".encode("ascii") # str in py2k, bytes in py3k | |
# | |
# Set VS_FF_PRERELEASE and DEBUG if Debug | |
# | |
def file_flags(debug): | |
if debug: | |
return 3 # VS_FF_DEBUG | VS_FF_PRERELEASE | |
return 0 | |
def file_type(is_dll): | |
if is_dll: | |
return 2 # VFT_DLL | |
return 1 # VFT_APP | |
def VS_FIXEDFILEINFO(maj, min, sub, build, debug=0, is_dll=1): | |
return struct.pack('lllllllllllll', | |
VS_FFI_SIGNATURE, # dwSignature | |
VS_FFI_STRUCVERSION, # dwStrucVersion | |
(maj << 16) | min, # dwFileVersionMS | |
(sub << 16) | build,# dwFileVersionLS | |
(maj << 16) | min, # dwProductVersionMS | |
(sub << 16) | build, # dwProductVersionLS | |
VS_FFI_FILEFLAGSMASK, # dwFileFlagsMask | |
file_flags(debug), # dwFileFlags | |
VOS_NT_WINDOWS32, # dwFileOS | |
file_type(is_dll), # dwFileType | |
0x00000000, # dwFileSubtype | |
0x00000000, # dwFileDateMS | |
0x00000000, # dwFileDateLS | |
) | |
def nullterm(s): | |
# get raw bytes for a NULL terminated unicode string. | |
return (str(s) + '\0').encode('unicode-internal') | |
def pad32(s, extra=2): | |
# extra is normally 2 to deal with wLength | |
l = 4 - ((len(s) + extra) & 3) | |
if l < 4: | |
return s + (null_byte * l) | |
return s | |
def addlen(s): | |
return struct.pack('h', len(s) + 2) + s | |
def String(key, value): | |
key = nullterm(key) | |
value = nullterm(value) | |
result = struct.pack('hh', len(value)//2, 1) # wValueLength, wType | |
result = result + key | |
result = pad32(result) + value | |
return addlen(result) | |
def StringTable(key, data): | |
key = nullterm(key) | |
result = struct.pack('hh', 0, 1) # wValueLength, wType | |
result = result + key | |
for k, v in data.items(): | |
result = result + String(k, v) | |
result = pad32(result) | |
return addlen(result) | |
def StringFileInfo(data): | |
result = struct.pack('hh', 0, 1) # wValueLength, wType | |
result = result + nullterm('StringFileInfo') | |
# result = pad32(result) + StringTable('040904b0', data) | |
result = pad32(result) + StringTable('040904E4', data) | |
return addlen(result) | |
def Var(key, value): | |
result = struct.pack('hh', len(value), 0) # wValueLength, wType | |
result = result + nullterm(key) | |
result = pad32(result) + value | |
return addlen(result) | |
def VarFileInfo(data): | |
result = struct.pack('hh', 0, 1) # wValueLength, wType | |
result = result + nullterm('VarFileInfo') | |
result = pad32(result) | |
for k, v in data.items(): | |
result = result + Var(k, v) | |
return addlen(result) | |
def VS_VERSION_INFO(maj, min, sub, build, sdata, vdata, debug=0, is_dll=1): | |
ffi = VS_FIXEDFILEINFO(maj, min, sub, build, debug, is_dll) | |
result = struct.pack('hh', len(ffi), 0) # wValueLength, wType | |
result = result + nullterm('VS_VERSION_INFO') | |
result = pad32(result) + ffi | |
result = pad32(result) + StringFileInfo(sdata) + VarFileInfo(vdata) | |
return addlen(result) | |
def stamp(pathname, options): | |
# For some reason, the API functions report success if the file is open | |
# but doesnt work! Try and open the file for writing, just to see if it is | |
# likely the stamp will work! | |
try: | |
f = open(pathname, "a+b") | |
f.close() | |
except IOError as why: | |
print("WARNING: File %s could not be opened - %s" % (pathname, why)) | |
ver = options.version | |
try: | |
bits = [int(i) for i in ver.split(".")] | |
vmaj, vmin, vsub, vbuild = bits | |
except (IndexError, TypeError, ValueError): | |
raise ValueError("--version must be a.b.c.d (all integers) - got %r" % ver) | |
ifn = options.internal_name | |
if not ifn: | |
ifn = os.path.basename(pathname) | |
ofn = options.original_filename | |
if not ofn: | |
ofn = os.path.basename(pathname) | |
sdata = { | |
'Comments' : options.comments, | |
'CompanyName' : options.company, | |
'FileDescription' : options.description, | |
'FileVersion' : ver, | |
'InternalName' : ifn, | |
'LegalCopyright' : options.copyright, | |
'LegalTrademarks' : options.trademarks, | |
'OriginalFilename' : ofn, | |
'ProductName' : options.product, | |
'ProductVersion' : ver, | |
} | |
vdata = { | |
'Translation' : struct.pack('hh', 0x409,1252), | |
} | |
is_dll = options.dll | |
if is_dll is None: | |
is_dll = os.path.splitext(pathname)[1].lower() in '.dll .pyd'.split() | |
is_debug = options.debug | |
if is_debug is None: | |
is_debug = os.path.splitext(pathname)[0].lower().endswith("_d") | |
# convert None to blank strings | |
for k, v in list(sdata.items()): | |
if v is None: | |
sdata[k] = "" | |
data = VS_VERSION_INFO(vmaj, vmin, vsub, vbuild, sdata, vdata, is_debug, is_dll) | |
language = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT) | |
handle = BeginUpdateResource(pathname, False) | |
name = 1 | |
res_type = 16 | |
UpdateResource(handle, res_type, name, language, data, len(data)) | |
EndUpdateResource(handle, False) | |
if options.verbose: | |
print("Stamped:", pathname) | |
if __name__ == '__main__': | |
parser = optparse.OptionParser("%prog [options] filespec ...", | |
description=__doc__) | |
parser.add_option("-q", "--quiet", | |
action="store_false", dest="verbose", default=True, | |
help="don't print status messages to stdout") | |
parser.add_option("", "--version", default="0.0.0.0", | |
help="The version number as m.n.s.b") | |
parser.add_option("", "--dll", | |
help="""Stamp the file as a DLL. Default is to look at the | |
file extension for .dll or .pyd.""") | |
parser.add_option("", "--debug", help="""Stamp the file as a debug binary.""") | |
parser.add_option("", "--product", help="""The product name to embed.""") | |
parser.add_option("", "--company", help="""The company name to embed.""") | |
parser.add_option("", "--trademarks", help="The trademark string to embed.") | |
parser.add_option("", "--comments", help="The comments string to embed.") | |
parser.add_option("", "--copyright", | |
help="""The copyright message string to embed.""") | |
parser.add_option("", "--description", metavar="DESC", | |
help="The description to embed.") | |
parser.add_option("", "--internal-name", metavar="NAME", | |
help="""The internal filename to embed. If not specified | |
the base filename is used.""") | |
parser.add_option("", "--original-filename", | |
help="""The original filename to embed. If not specified | |
the base filename is used.""") | |
options, args = parser.parse_args() | |
if not args: | |
parser.error("You must supply a file to stamp. Use --help for details.") | |
for g in args: | |
for f in glob.glob(g): | |
stamp(f, options) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment