__init__.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. # Copyright (C) 2005 Martin v. Löwis
  2. # Licensed to PSF under a Contributor Agreement.
  3. from _msi import *
  4. import fnmatch
  5. import os
  6. import re
  7. import string
  8. import sys
  9. import warnings
  10. warnings._deprecated(__name__, remove=(3, 13))
  11. AMD64 = "AMD64" in sys.version
  12. # Keep msilib.Win64 around to preserve backwards compatibility.
  13. Win64 = AMD64
  14. # Partially taken from Wine
  15. datasizemask= 0x00ff
  16. type_valid= 0x0100
  17. type_localizable= 0x0200
  18. typemask= 0x0c00
  19. type_long= 0x0000
  20. type_short= 0x0400
  21. type_string= 0x0c00
  22. type_binary= 0x0800
  23. type_nullable= 0x1000
  24. type_key= 0x2000
  25. # XXX temporary, localizable?
  26. knownbits = datasizemask | type_valid | type_localizable | \
  27. typemask | type_nullable | type_key
  28. class Table:
  29. def __init__(self, name):
  30. self.name = name
  31. self.fields = []
  32. def add_field(self, index, name, type):
  33. self.fields.append((index,name,type))
  34. def sql(self):
  35. fields = []
  36. keys = []
  37. self.fields.sort()
  38. fields = [None]*len(self.fields)
  39. for index, name, type in self.fields:
  40. index -= 1
  41. unk = type & ~knownbits
  42. if unk:
  43. print("%s.%s unknown bits %x" % (self.name, name, unk))
  44. size = type & datasizemask
  45. dtype = type & typemask
  46. if dtype == type_string:
  47. if size:
  48. tname="CHAR(%d)" % size
  49. else:
  50. tname="CHAR"
  51. elif dtype == type_short:
  52. assert size==2
  53. tname = "SHORT"
  54. elif dtype == type_long:
  55. assert size==4
  56. tname="LONG"
  57. elif dtype == type_binary:
  58. assert size==0
  59. tname="OBJECT"
  60. else:
  61. tname="unknown"
  62. print("%s.%sunknown integer type %d" % (self.name, name, size))
  63. if type & type_nullable:
  64. flags = ""
  65. else:
  66. flags = " NOT NULL"
  67. if type & type_localizable:
  68. flags += " LOCALIZABLE"
  69. fields[index] = "`%s` %s%s" % (name, tname, flags)
  70. if type & type_key:
  71. keys.append("`%s`" % name)
  72. fields = ", ".join(fields)
  73. keys = ", ".join(keys)
  74. return "CREATE TABLE %s (%s PRIMARY KEY %s)" % (self.name, fields, keys)
  75. def create(self, db):
  76. v = db.OpenView(self.sql())
  77. v.Execute(None)
  78. v.Close()
  79. class _Unspecified:pass
  80. def change_sequence(seq, action, seqno=_Unspecified, cond = _Unspecified):
  81. "Change the sequence number of an action in a sequence list"
  82. for i in range(len(seq)):
  83. if seq[i][0] == action:
  84. if cond is _Unspecified:
  85. cond = seq[i][1]
  86. if seqno is _Unspecified:
  87. seqno = seq[i][2]
  88. seq[i] = (action, cond, seqno)
  89. return
  90. raise ValueError("Action not found in sequence")
  91. def add_data(db, table, values):
  92. v = db.OpenView("SELECT * FROM `%s`" % table)
  93. count = v.GetColumnInfo(MSICOLINFO_NAMES).GetFieldCount()
  94. r = CreateRecord(count)
  95. for value in values:
  96. assert len(value) == count, value
  97. for i in range(count):
  98. field = value[i]
  99. if isinstance(field, int):
  100. r.SetInteger(i+1,field)
  101. elif isinstance(field, str):
  102. r.SetString(i+1,field)
  103. elif field is None:
  104. pass
  105. elif isinstance(field, Binary):
  106. r.SetStream(i+1, field.name)
  107. else:
  108. raise TypeError("Unsupported type %s" % field.__class__.__name__)
  109. try:
  110. v.Modify(MSIMODIFY_INSERT, r)
  111. except Exception:
  112. raise MSIError("Could not insert "+repr(values)+" into "+table)
  113. r.ClearData()
  114. v.Close()
  115. def add_stream(db, name, path):
  116. v = db.OpenView("INSERT INTO _Streams (Name, Data) VALUES ('%s', ?)" % name)
  117. r = CreateRecord(1)
  118. r.SetStream(1, path)
  119. v.Execute(r)
  120. v.Close()
  121. def init_database(name, schema,
  122. ProductName, ProductCode, ProductVersion,
  123. Manufacturer):
  124. try:
  125. os.unlink(name)
  126. except OSError:
  127. pass
  128. ProductCode = ProductCode.upper()
  129. # Create the database
  130. db = OpenDatabase(name, MSIDBOPEN_CREATE)
  131. # Create the tables
  132. for t in schema.tables:
  133. t.create(db)
  134. # Fill the validation table
  135. add_data(db, "_Validation", schema._Validation_records)
  136. # Initialize the summary information, allowing atmost 20 properties
  137. si = db.GetSummaryInformation(20)
  138. si.SetProperty(PID_TITLE, "Installation Database")
  139. si.SetProperty(PID_SUBJECT, ProductName)
  140. si.SetProperty(PID_AUTHOR, Manufacturer)
  141. if AMD64:
  142. si.SetProperty(PID_TEMPLATE, "x64;1033")
  143. else:
  144. si.SetProperty(PID_TEMPLATE, "Intel;1033")
  145. si.SetProperty(PID_REVNUMBER, gen_uuid())
  146. si.SetProperty(PID_WORDCOUNT, 2) # long file names, compressed, original media
  147. si.SetProperty(PID_PAGECOUNT, 200)
  148. si.SetProperty(PID_APPNAME, "Python MSI Library")
  149. # XXX more properties
  150. si.Persist()
  151. add_data(db, "Property", [
  152. ("ProductName", ProductName),
  153. ("ProductCode", ProductCode),
  154. ("ProductVersion", ProductVersion),
  155. ("Manufacturer", Manufacturer),
  156. ("ProductLanguage", "1033")])
  157. db.Commit()
  158. return db
  159. def add_tables(db, module):
  160. for table in module.tables:
  161. add_data(db, table, getattr(module, table))
  162. def make_id(str):
  163. identifier_chars = string.ascii_letters + string.digits + "._"
  164. str = "".join([c if c in identifier_chars else "_" for c in str])
  165. if str[0] in (string.digits + "."):
  166. str = "_" + str
  167. assert re.match("^[A-Za-z_][A-Za-z0-9_.]*$", str), "FILE"+str
  168. return str
  169. def gen_uuid():
  170. return "{"+UuidCreate().upper()+"}"
  171. class CAB:
  172. def __init__(self, name):
  173. self.name = name
  174. self.files = []
  175. self.filenames = set()
  176. self.index = 0
  177. def gen_id(self, file):
  178. logical = _logical = make_id(file)
  179. pos = 1
  180. while logical in self.filenames:
  181. logical = "%s.%d" % (_logical, pos)
  182. pos += 1
  183. self.filenames.add(logical)
  184. return logical
  185. def append(self, full, file, logical):
  186. if os.path.isdir(full):
  187. return
  188. if not logical:
  189. logical = self.gen_id(file)
  190. self.index += 1
  191. self.files.append((full, logical))
  192. return self.index, logical
  193. def commit(self, db):
  194. from tempfile import mktemp
  195. filename = mktemp()
  196. FCICreate(filename, self.files)
  197. add_data(db, "Media",
  198. [(1, self.index, None, "#"+self.name, None, None)])
  199. add_stream(db, self.name, filename)
  200. os.unlink(filename)
  201. db.Commit()
  202. _directories = set()
  203. class Directory:
  204. def __init__(self, db, cab, basedir, physical, _logical, default, componentflags=None):
  205. """Create a new directory in the Directory table. There is a current component
  206. at each point in time for the directory, which is either explicitly created
  207. through start_component, or implicitly when files are added for the first
  208. time. Files are added into the current component, and into the cab file.
  209. To create a directory, a base directory object needs to be specified (can be
  210. None), the path to the physical directory, and a logical directory name.
  211. Default specifies the DefaultDir slot in the directory table. componentflags
  212. specifies the default flags that new components get."""
  213. index = 1
  214. _logical = make_id(_logical)
  215. logical = _logical
  216. while logical in _directories:
  217. logical = "%s%d" % (_logical, index)
  218. index += 1
  219. _directories.add(logical)
  220. self.db = db
  221. self.cab = cab
  222. self.basedir = basedir
  223. self.physical = physical
  224. self.logical = logical
  225. self.component = None
  226. self.short_names = set()
  227. self.ids = set()
  228. self.keyfiles = {}
  229. self.componentflags = componentflags
  230. if basedir:
  231. self.absolute = os.path.join(basedir.absolute, physical)
  232. blogical = basedir.logical
  233. else:
  234. self.absolute = physical
  235. blogical = None
  236. add_data(db, "Directory", [(logical, blogical, default)])
  237. def start_component(self, component = None, feature = None, flags = None, keyfile = None, uuid=None):
  238. """Add an entry to the Component table, and make this component the current for this
  239. directory. If no component name is given, the directory name is used. If no feature
  240. is given, the current feature is used. If no flags are given, the directory's default
  241. flags are used. If no keyfile is given, the KeyPath is left null in the Component
  242. table."""
  243. if flags is None:
  244. flags = self.componentflags
  245. if uuid is None:
  246. uuid = gen_uuid()
  247. else:
  248. uuid = uuid.upper()
  249. if component is None:
  250. component = self.logical
  251. self.component = component
  252. if AMD64:
  253. flags |= 256
  254. if keyfile:
  255. keyid = self.cab.gen_id(keyfile)
  256. self.keyfiles[keyfile] = keyid
  257. else:
  258. keyid = None
  259. add_data(self.db, "Component",
  260. [(component, uuid, self.logical, flags, None, keyid)])
  261. if feature is None:
  262. feature = current_feature
  263. add_data(self.db, "FeatureComponents",
  264. [(feature.id, component)])
  265. def make_short(self, file):
  266. oldfile = file
  267. file = file.replace('+', '_')
  268. file = ''.join(c for c in file if not c in r' "/\[]:;=,')
  269. parts = file.split(".")
  270. if len(parts) > 1:
  271. prefix = "".join(parts[:-1]).upper()
  272. suffix = parts[-1].upper()
  273. if not prefix:
  274. prefix = suffix
  275. suffix = None
  276. else:
  277. prefix = file.upper()
  278. suffix = None
  279. if len(parts) < 3 and len(prefix) <= 8 and file == oldfile and (
  280. not suffix or len(suffix) <= 3):
  281. if suffix:
  282. file = prefix+"."+suffix
  283. else:
  284. file = prefix
  285. else:
  286. file = None
  287. if file is None or file in self.short_names:
  288. prefix = prefix[:6]
  289. if suffix:
  290. suffix = suffix[:3]
  291. pos = 1
  292. while 1:
  293. if suffix:
  294. file = "%s~%d.%s" % (prefix, pos, suffix)
  295. else:
  296. file = "%s~%d" % (prefix, pos)
  297. if file not in self.short_names: break
  298. pos += 1
  299. assert pos < 10000
  300. if pos in (10, 100, 1000):
  301. prefix = prefix[:-1]
  302. self.short_names.add(file)
  303. assert not re.search(r'[\?|><:/*"+,;=\[\]]', file) # restrictions on short names
  304. return file
  305. def add_file(self, file, src=None, version=None, language=None):
  306. """Add a file to the current component of the directory, starting a new one
  307. if there is no current component. By default, the file name in the source
  308. and the file table will be identical. If the src file is specified, it is
  309. interpreted relative to the current directory. Optionally, a version and a
  310. language can be specified for the entry in the File table."""
  311. if not self.component:
  312. self.start_component(self.logical, current_feature, 0)
  313. if not src:
  314. # Allow relative paths for file if src is not specified
  315. src = file
  316. file = os.path.basename(file)
  317. absolute = os.path.join(self.absolute, src)
  318. assert not re.search(r'[\?|><:/*]"', file) # restrictions on long names
  319. if file in self.keyfiles:
  320. logical = self.keyfiles[file]
  321. else:
  322. logical = None
  323. sequence, logical = self.cab.append(absolute, file, logical)
  324. assert logical not in self.ids
  325. self.ids.add(logical)
  326. short = self.make_short(file)
  327. full = "%s|%s" % (short, file)
  328. filesize = os.stat(absolute).st_size
  329. # constants.msidbFileAttributesVital
  330. # Compressed omitted, since it is the database default
  331. # could add r/o, system, hidden
  332. attributes = 512
  333. add_data(self.db, "File",
  334. [(logical, self.component, full, filesize, version,
  335. language, attributes, sequence)])
  336. #if not version:
  337. # # Add hash if the file is not versioned
  338. # filehash = FileHash(absolute, 0)
  339. # add_data(self.db, "MsiFileHash",
  340. # [(logical, 0, filehash.IntegerData(1),
  341. # filehash.IntegerData(2), filehash.IntegerData(3),
  342. # filehash.IntegerData(4))])
  343. # Automatically remove .pyc files on uninstall (2)
  344. # XXX: adding so many RemoveFile entries makes installer unbelievably
  345. # slow. So instead, we have to use wildcard remove entries
  346. if file.endswith(".py"):
  347. add_data(self.db, "RemoveFile",
  348. [(logical+"c", self.component, "%sC|%sc" % (short, file),
  349. self.logical, 2),
  350. (logical+"o", self.component, "%sO|%so" % (short, file),
  351. self.logical, 2)])
  352. return logical
  353. def glob(self, pattern, exclude = None):
  354. """Add a list of files to the current component as specified in the
  355. glob pattern. Individual files can be excluded in the exclude list."""
  356. try:
  357. files = os.listdir(self.absolute)
  358. except OSError:
  359. return []
  360. if pattern[:1] != '.':
  361. files = (f for f in files if f[0] != '.')
  362. files = fnmatch.filter(files, pattern)
  363. for f in files:
  364. if exclude and f in exclude: continue
  365. self.add_file(f)
  366. return files
  367. def remove_pyc(self):
  368. "Remove .pyc files on uninstall"
  369. add_data(self.db, "RemoveFile",
  370. [(self.component+"c", self.component, "*.pyc", self.logical, 2)])
  371. class Binary:
  372. def __init__(self, fname):
  373. self.name = fname
  374. def __repr__(self):
  375. return 'msilib.Binary(os.path.join(dirname,"%s"))' % self.name
  376. class Feature:
  377. def __init__(self, db, id, title, desc, display, level = 1,
  378. parent=None, directory = None, attributes=0):
  379. self.id = id
  380. if parent:
  381. parent = parent.id
  382. add_data(db, "Feature",
  383. [(id, parent, title, desc, display,
  384. level, directory, attributes)])
  385. def set_current(self):
  386. global current_feature
  387. current_feature = self
  388. class Control:
  389. def __init__(self, dlg, name):
  390. self.dlg = dlg
  391. self.name = name
  392. def event(self, event, argument, condition = "1", ordering = None):
  393. add_data(self.dlg.db, "ControlEvent",
  394. [(self.dlg.name, self.name, event, argument,
  395. condition, ordering)])
  396. def mapping(self, event, attribute):
  397. add_data(self.dlg.db, "EventMapping",
  398. [(self.dlg.name, self.name, event, attribute)])
  399. def condition(self, action, condition):
  400. add_data(self.dlg.db, "ControlCondition",
  401. [(self.dlg.name, self.name, action, condition)])
  402. class RadioButtonGroup(Control):
  403. def __init__(self, dlg, name, property):
  404. self.dlg = dlg
  405. self.name = name
  406. self.property = property
  407. self.index = 1
  408. def add(self, name, x, y, w, h, text, value = None):
  409. if value is None:
  410. value = name
  411. add_data(self.dlg.db, "RadioButton",
  412. [(self.property, self.index, value,
  413. x, y, w, h, text, None)])
  414. self.index += 1
  415. class Dialog:
  416. def __init__(self, db, name, x, y, w, h, attr, title, first, default, cancel):
  417. self.db = db
  418. self.name = name
  419. self.x, self.y, self.w, self.h = x,y,w,h
  420. add_data(db, "Dialog", [(name, x,y,w,h,attr,title,first,default,cancel)])
  421. def control(self, name, type, x, y, w, h, attr, prop, text, next, help):
  422. add_data(self.db, "Control",
  423. [(self.name, name, type, x, y, w, h, attr, prop, text, next, help)])
  424. return Control(self, name)
  425. def text(self, name, x, y, w, h, attr, text):
  426. return self.control(name, "Text", x, y, w, h, attr, None,
  427. text, None, None)
  428. def bitmap(self, name, x, y, w, h, text):
  429. return self.control(name, "Bitmap", x, y, w, h, 1, None, text, None, None)
  430. def line(self, name, x, y, w, h):
  431. return self.control(name, "Line", x, y, w, h, 1, None, None, None, None)
  432. def pushbutton(self, name, x, y, w, h, attr, text, next):
  433. return self.control(name, "PushButton", x, y, w, h, attr, None, text, next, None)
  434. def radiogroup(self, name, x, y, w, h, attr, prop, text, next):
  435. add_data(self.db, "Control",
  436. [(self.name, name, "RadioButtonGroup",
  437. x, y, w, h, attr, prop, text, next, None)])
  438. return RadioButtonGroup(self, name, prop)
  439. def checkbox(self, name, x, y, w, h, attr, prop, text, next):
  440. return self.control(name, "CheckBox", x, y, w, h, attr, prop, text, next, None)