Skip to content

Instantly share code, notes, and snippets.

@cas--
Created September 21, 2018 15:40
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save cas--/5168013c49a87ce848086a1b0d3c770e to your computer and use it in GitHub Desktop.
Save cas--/5168013c49a87ce848086a1b0d3c770e to your computer and use it in GitHub Desktop.
Stamp windows exe files with version information
""" 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