Update bdist_msi so that the generated MSIs for pure Python modules can install to any version of Python, like the generated EXEs from bdist_wininst. (Previously, you had to create a new MSI for each version of Python.)

This commit is contained in:
Steven Bethard 2009-05-05 01:31:22 +00:00
parent 7c67b03051
commit ab538fc2b2

View file

@ -117,6 +117,12 @@ class bdist_msi (Command):
boolean_options = ['keep-temp', 'no-target-compile', 'no-target-optimize', boolean_options = ['keep-temp', 'no-target-compile', 'no-target-optimize',
'skip-build'] 'skip-build']
all_versions = ['2.0', '2.1', '2.2', '2.3', '2.4',
'2.5', '2.6', '2.7', '2.8', '2.9',
'3.0', '3.1', '3.2', '3.3', '3.4',
'3.5', '3.6', '3.7', '3.8', '3.9']
other_version = 'X'
def initialize_options (self): def initialize_options (self):
self.bdist_dir = None self.bdist_dir = None
self.plat_name = None self.plat_name = None
@ -128,6 +134,7 @@ class bdist_msi (Command):
self.skip_build = 0 self.skip_build = 0
self.install_script = None self.install_script = None
self.pre_install_script = None self.pre_install_script = None
self.versions = None
def finalize_options (self): def finalize_options (self):
if self.bdist_dir is None: if self.bdist_dir is None:
@ -135,13 +142,14 @@ class bdist_msi (Command):
self.bdist_dir = os.path.join(bdist_base, 'msi') self.bdist_dir = os.path.join(bdist_base, 'msi')
short_version = get_python_version() short_version = get_python_version()
if self.target_version: if self.target_version:
self.versions = [self.target_version]
if not self.skip_build and self.distribution.has_ext_modules()\ if not self.skip_build and self.distribution.has_ext_modules()\
and self.target_version != short_version: and self.target_version != short_version:
raise DistutilsOptionError, \ raise DistutilsOptionError, \
"target version can only be %s, or the '--skip_build'" \ "target version can only be %s, or the '--skip_build'" \
" option must be specified" % (short_version,) " option must be specified" % (short_version,)
else: else:
self.target_version = short_version self.versions = list(self.all_versions)
self.set_undefined_options('bdist', self.set_undefined_options('bdist',
('dist_dir', 'dist_dir'), ('dist_dir', 'dist_dir'),
@ -223,8 +231,11 @@ class bdist_msi (Command):
# Prefix ProductName with Python x.y, so that # Prefix ProductName with Python x.y, so that
# it sorts together with the other Python packages # it sorts together with the other Python packages
# in Add-Remove-Programs (APR) # in Add-Remove-Programs (APR)
product_name = "Python %s %s" % (self.target_version, fullname = self.distribution.get_fullname()
self.distribution.get_fullname()) if self.target_version:
product_name = "Python %s %s" % (self.target_version, fullname)
else:
product_name = "Python %s" % (fullname)
self.db = msilib.init_database(installer_name, schema, self.db = msilib.init_database(installer_name, schema,
product_name, msilib.gen_uuid(), product_name, msilib.gen_uuid(),
sversion, author) sversion, author)
@ -245,7 +256,8 @@ class bdist_msi (Command):
self.db.Commit() self.db.Commit()
if hasattr(self.distribution, 'dist_files'): if hasattr(self.distribution, 'dist_files'):
self.distribution.dist_files.append(('bdist_msi', self.target_version, fullname)) tup = 'bdist_msi', self.target_version or 'any', fullname
self.distribution.dist_files.append(tup)
if not self.keep_temp: if not self.keep_temp:
remove_tree(self.bdist_dir, dry_run=self.dry_run) remove_tree(self.bdist_dir, dry_run=self.dry_run)
@ -253,65 +265,121 @@ class bdist_msi (Command):
def add_files(self): def add_files(self):
db = self.db db = self.db
cab = msilib.CAB("distfiles") cab = msilib.CAB("distfiles")
f = Feature(db, "default", "Default Feature", "Everything", 1, directory="TARGETDIR")
f.set_current()
rootdir = os.path.abspath(self.bdist_dir) rootdir = os.path.abspath(self.bdist_dir)
root = Directory(db, cab, None, rootdir, "TARGETDIR", "SourceDir") root = Directory(db, cab, None, rootdir, "TARGETDIR", "SourceDir")
f = Feature(db, "Python", "Python", "Everything",
0, 1, directory="TARGETDIR")
items = [(f, root, '')]
for version in self.versions + [self.other_version]:
target = "TARGETDIR" + version
name = default = "Python" + version
desc = "Everything"
if version is self.other_version:
title = "Python from another location"
level = 2
else:
title = "Python %s from registry" % version
level = 1
f = Feature(db, name, title, desc, 1, level, directory=target)
dir = Directory(db, cab, root, rootdir, target, default)
items.append((f, dir, version))
db.Commit() db.Commit()
todo = [root]
while todo: seen = {}
dir = todo.pop() for feature, dir, version in items:
for file in os.listdir(dir.absolute): todo = [dir]
afile = os.path.join(dir.absolute, file) while todo:
if os.path.isdir(afile): dir = todo.pop()
newdir = Directory(db, cab, dir, file, file, "%s|%s" % (dir.make_short(file), file)) for file in os.listdir(dir.absolute):
todo.append(newdir) afile = os.path.join(dir.absolute, file)
else: if os.path.isdir(afile):
key = dir.add_file(file) short = "%s|%s" % (dir.make_short(file), file)
if file==self.install_script: default = file + version
if self.install_script_key: newdir = Directory(db, cab, dir, file, default, short)
raise DistutilsOptionError, "Multiple files with name %s" % file todo.append(newdir)
self.install_script_key = '[#%s]' % key else:
if not dir.component:
dir.start_component(dir.logical, feature, 0)
if afile not in seen:
key = seen[afile] = dir.add_file(file)
if file==self.install_script:
if self.install_script_key:
raise DistutilsOptionError(
"Multiple files with name %s" % file)
self.install_script_key = '[#%s]' % key
else:
key = seen[afile]
add_data(self.db, "DuplicateFile",
[(key + version, dir.component, key, None, dir.logical)])
cab.commit(db) cab.commit(db)
def add_find_python(self): def add_find_python(self):
"""Adds code to the installer to compute the location of Python. """Adds code to the installer to compute the location of Python.
Properties PYTHON.MACHINE, PYTHON.USER, PYTHONDIR and PYTHON will be set
in both the execute and UI sequences; PYTHONDIR will be set from Properties PYTHON.MACHINE.X.Y and PYTHON.USER.X.Y will be set from the
PYTHON.USER if defined, else from PYTHON.MACHINE. registry for each version of Python.
PYTHON is PYTHONDIR\python.exe"""
install_path = r"SOFTWARE\Python\PythonCore\%s\InstallPath" % self.target_version Properties TARGETDIRX.Y will be set from PYTHON.USER.X.Y if defined,
add_data(self.db, "RegLocator", else from PYTHON.MACHINE.X.Y.
[("python.machine", 2, install_path, None, 2),
("python.user", 1, install_path, None, 2)]) Properties PYTHONX.Y will be set to TARGETDIRX.Y\\python.exe"""
add_data(self.db, "AppSearch",
[("PYTHON.MACHINE", "python.machine"), start = 402
("PYTHON.USER", "python.user")]) for ver in self.versions:
add_data(self.db, "CustomAction", install_path = r"SOFTWARE\Python\PythonCore\%s\InstallPath" % ver
[("PythonFromMachine", 51+256, "PYTHONDIR", "[PYTHON.MACHINE]"), machine_reg = "python.machine." + ver
("PythonFromUser", 51+256, "PYTHONDIR", "[PYTHON.USER]"), user_reg = "python.user." + ver
("PythonExe", 51+256, "PYTHON", "[PYTHONDIR]\\python.exe"), machine_prop = "PYTHON.MACHINE." + ver
("InitialTargetDir", 51+256, "TARGETDIR", "[PYTHONDIR]")]) user_prop = "PYTHON.USER." + ver
add_data(self.db, "InstallExecuteSequence", machine_action = "PythonFromMachine" + ver
[("PythonFromMachine", "PYTHON.MACHINE", 401), user_action = "PythonFromUser" + ver
("PythonFromUser", "PYTHON.USER", 402), exe_action = "PythonExe" + ver
("PythonExe", None, 403), target_dir_prop = "TARGETDIR" + ver
("InitialTargetDir", 'TARGETDIR=""', 404), exe_prop = "PYTHON" + ver
]) add_data(self.db, "RegLocator",
add_data(self.db, "InstallUISequence", [(machine_reg, 2, install_path, None, 2),
[("PythonFromMachine", "PYTHON.MACHINE", 401), (user_reg, 1, install_path, None, 2)])
("PythonFromUser", "PYTHON.USER", 402), add_data(self.db, "AppSearch",
("PythonExe", None, 403), [(machine_prop, machine_reg),
("InitialTargetDir", 'TARGETDIR=""', 404), (user_prop, user_reg)])
]) add_data(self.db, "CustomAction",
[(machine_action, 51+256, target_dir_prop, "[" + machine_prop + "]"),
(user_action, 51+256, target_dir_prop, "[" + user_prop + "]"),
(exe_action, 51+256, exe_prop, "[" + target_dir_prop + "]\\python.exe"),
])
add_data(self.db, "InstallExecuteSequence",
[(machine_action, machine_prop, start),
(user_action, user_prop, start + 1),
(exe_action, None, start + 2),
])
add_data(self.db, "InstallUISequence",
[(machine_action, machine_prop, start),
(user_action, user_prop, start + 1),
(exe_action, None, start + 2),
])
add_data(self.db, "Condition",
[("Python" + ver, 0, "NOT TARGETDIR" + ver)])
start += 4
assert start < 500
def add_scripts(self): def add_scripts(self):
if self.install_script: if self.install_script:
add_data(self.db, "CustomAction", start = 6800
[("install_script", 50, "PYTHON", self.install_script_key)]) for ver in self.versions + [self.other_version]:
add_data(self.db, "InstallExecuteSequence", install_action = "install_script." + ver
[("install_script", "NOT Installed", 6800)]) exe_prop = "PYTHON" + ver
add_data(self.db, "CustomAction",
[(install_action, 50, exe_prop, self.install_script_key)])
add_data(self.db, "InstallExecuteSequence",
[(install_action, "&Python%s=3" % ver, start)])
start += 1
# XXX pre-install scripts are currently refused in finalize_options()
# but if this feature is completed, it will also need to add
# entries for each version as the above code does
if self.pre_install_script: if self.pre_install_script:
scriptfn = os.path.join(self.bdist_dir, "preinstall.bat") scriptfn = os.path.join(self.bdist_dir, "preinstall.bat")
f = open(scriptfn, "w") f = open(scriptfn, "w")
@ -375,7 +443,7 @@ class bdist_msi (Command):
[("PrepareDlg", "Not Privileged or Windows9x or Installed", 140), [("PrepareDlg", "Not Privileged or Windows9x or Installed", 140),
("WhichUsersDlg", "Privileged and not Windows9x and not Installed", 141), ("WhichUsersDlg", "Privileged and not Windows9x and not Installed", 141),
# In the user interface, assume all-users installation if privileged. # In the user interface, assume all-users installation if privileged.
("SelectDirectoryDlg", "Not Installed", 1230), ("SelectFeaturesDlg", "Not Installed", 1230),
# XXX no support for resume installations yet # XXX no support for resume installations yet
#("ResumeDlg", "Installed AND (RESUME OR Preselected)", 1240), #("ResumeDlg", "Installed AND (RESUME OR Preselected)", 1240),
("MaintenanceTypeDlg", "Installed AND NOT RESUME AND NOT Preselected", 1250), ("MaintenanceTypeDlg", "Installed AND NOT RESUME AND NOT Preselected", 1250),
@ -498,33 +566,49 @@ class bdist_msi (Command):
c.event("SpawnDialog", "CancelDlg") c.event("SpawnDialog", "CancelDlg")
##################################################################### #####################################################################
# Target directory selection # Feature (Python directory) selection
seldlg = PyDialog(db, "SelectDirectoryDlg", x, y, w, h, modal, title, seldlg = PyDialog(db, "SelectFeaturesDlg", x, y, w, h, modal, title,
"Next", "Next", "Cancel") "Next", "Next", "Cancel")
seldlg.title("Select Destination Directory") seldlg.title("Select Python Installations")
version = sys.version[:3]+" " seldlg.text("Hint", 15, 30, 300, 20, 3,
seldlg.text("Hint", 15, 30, 300, 40, 3, "Select the Python locations where %s should be installed."
"The destination directory should contain a Python %sinstallation" % version) % self.distribution.get_fullname())
seldlg.back("< Back", None, active=0) seldlg.back("< Back", None, active=0)
c = seldlg.next("Next >", "Cancel") c = seldlg.next("Next >", "Cancel")
c.event("SetTargetPath", "TARGETDIR", ordering=1) order = 1
c.event("SpawnWaitDialog", "WaitForCostingDlg", ordering=2) c.event("[TARGETDIR]", "[SourceDir]", ordering=order)
c.event("EndDialog", "Return", ordering=3) for version in self.versions + [self.other_version]:
order += 1
c = seldlg.cancel("Cancel", "DirectoryCombo") c.event("[TARGETDIR]", "[TARGETDIR%s]" % version,
"FEATURE_SELECTED AND &Python%s=3" % version,
ordering=order)
c.event("SpawnWaitDialog", "WaitForCostingDlg", ordering=order + 1)
c.event("EndDialog", "Return", ordering=order + 2)
c = seldlg.cancel("Cancel", "Features")
c.event("SpawnDialog", "CancelDlg") c.event("SpawnDialog", "CancelDlg")
seldlg.control("DirectoryCombo", "DirectoryCombo", 15, 70, 272, 80, 393219, c = seldlg.control("Features", "SelectionTree", 15, 60, 300, 120, 3,
"TARGETDIR", None, "DirectoryList", None) "FEATURE", None, "PathEdit", None)
seldlg.control("DirectoryList", "DirectoryList", 15, 90, 308, 136, 3, "TARGETDIR", c.event("[FEATURE_SELECTED]", "1")
None, "PathEdit", None) ver = self.other_version
seldlg.control("PathEdit", "PathEdit", 15, 230, 306, 16, 3, "TARGETDIR", None, "Next", None) install_other_cond = "FEATURE_SELECTED AND &Python%s=3" % ver
c = seldlg.pushbutton("Up", 306, 70, 18, 18, 3, "Up", None) dont_install_other_cond = "FEATURE_SELECTED AND &Python%s<>3" % ver
c.event("DirectoryListUp", "0")
c = seldlg.pushbutton("NewDir", 324, 70, 30, 18, 3, "New", None) c = seldlg.text("Other", 15, 200, 300, 15, 3,
c.event("DirectoryListNew", "0") "Provide an alternate Python location")
c.condition("Enable", install_other_cond)
c.condition("Show", install_other_cond)
c.condition("Disable", dont_install_other_cond)
c.condition("Hide", dont_install_other_cond)
c = seldlg.control("PathEdit", "PathEdit", 15, 215, 300, 16, 1,
"TARGETDIR" + ver, None, "Next", None)
c.condition("Enable", install_other_cond)
c.condition("Show", install_other_cond)
c.condition("Disable", dont_install_other_cond)
c.condition("Hide", dont_install_other_cond)
##################################################################### #####################################################################
# Disk cost # Disk cost
@ -640,7 +724,10 @@ class bdist_msi (Command):
def get_installer_filename(self, fullname): def get_installer_filename(self, fullname):
# Factored out to allow overriding in subclasses # Factored out to allow overriding in subclasses
base_name = "%s.%s-py%s.msi" % (fullname, self.plat_name, if self.target_version:
self.target_version) base_name = "%s.%s-py%s.msi" % (fullname, self.plat_name,
self.target_version)
else:
base_name = "%s.%s.msi" % (fullname, self.plat_name)
installer_name = os.path.join(self.dist_dir, base_name) installer_name = os.path.join(self.dist_dir, base_name)
return installer_name return installer_name