""" Stamp a Win32 binary with version information. """ import glob import optparse import os import struct import sys from win32api import BeginUpdateResource, EndUpdateResource, UpdateResource VS_FFI_SIGNATURE = -17890115 # 0xFEEF04BD VS_FFI_STRUCVERSION = 0x00010000 VS_FFI_FILEFLAGSMASK = 0x0000003F VOS_NT_WINDOWS32 = 0x00040004 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. if sys.version_info[:2] < (3, 7): return (str(s) + "\0").encode("unicode-internal") else: return (str(s) + "\0").encode("utf-16le") 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 ofn is None: 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] = "" vs = VS_VERSION_INFO(vmaj, vmin, vsub, vbuild, sdata, vdata, is_debug, is_dll) h = BeginUpdateResource(pathname, 0) UpdateResource(h, 16, 1, vs) EndUpdateResource(h, 0) 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)