Repository URL to install this package:
|
Version:
15.1.0-1 ▾
|
# (c) Copyright 2009-2013, 2015. CodeWeavers, Inc.
import errno
import os
import os.path
import shutil
import stat
import zipfile
import tarfile
import tempfile
import threading
import traceback
import cxlog
import cxulog
import cxutils
import globtree
import bottlequery
import cxaiebase
import cxaiemedia
import appdetector
import distversion
# for localization
from cxutils import cxgettext as _
def tar_member_depth(member):
"Returns the number of directories leading up to this file"
return os.path.normpath(member.name).count('/')
#####
#
# The core application installation task
#
#####
class AIECore(cxaiebase.AIETask):
"""Installs a specific application."""
#####
#
# Limit concurrency
#
#####
def _getniceness(self):
"""Start downloads before all else."""
return self.depth
niceness = property(_getniceness)
def can_run(self):
"""Returns True if this task is runnable and no other instance of it
is running.
"""
if 'AIECore' in self.scheduler.userdata:
return False
return cxaiebase.AIETask.can_run(self)
def schedule(self):
cxaiebase.AIETask.schedule(self)
self.scheduler.userdata['AIECore'] = self
def done(self):
cxaiebase.AIETask.done(self)
try:
del self.scheduler.userdata['AIECore']
except KeyError:
cxlog.err(cxlog.to_str(self) + ": AIECore should have been set")
#####
#
# Initialization
#
#####
def __init__(self, engine, installer):
cxaiebase.AIETask.__init__(self, engine, _("Installing %s") % installer.parent.name)
self.installer = installer
# Internal housekeeping variables
self._has_printed_install_log_err = False
# Related objects to download the application or handle CDs
self.download = None
self.insert_media = None
self.scan_media = None
self.release_media = None
self.canceled = False
# Nearest AIECore objects with an associated media
self.parent_media = set()
self.cleanup_lock = threading.Lock()
# depth is 0 for the top profile, -1 for those it depends on, etc.
# Note that depth always reflects the depth of the longest path from
# the top-level profile.
self.depth = 0
# Stores the state information used at run time
self.state = {}
def _getname(self):
"""A helper function that returns the associated profile name."""
return self.installer.parent.name
name = property(_getname)
def _getaiecore(self):
"""A helper function that makes AIECore more similar to other types of
tasks.
"""
return self
aiecore = property(_getaiecore)
def __unicode__(self):
return "%s(%s)" % (self.__class__.__name__, self.name)
def create_download_task(self):
self.download = cxaiemedia.AIEDownload(self)
def create_media_tasks(self):
self.insert_media = cxaiemedia.AIEInsertMedia(self)
self.scan_media = cxaiemedia.AIEScanMedia(self)
self.scan_media.add_dependency(self.insert_media)
self.release_media = cxaiemedia.AIEReleaseMedia(self)
self.release_media.add_dependency(self.scan_media)
def run_routines(self, prefix, routines):
if 'iter' in self.state:
iterator = self.state['routine_iterator']
else:
iterator = iter(routines)
self.state['routine_iterator'] = iterator
for routine in iterator:
skip_routine = 'no_' + routine
if skip_routine in self.state:
continue
if self.canceled:
return True
try:
func = AIECore.__dict__['_%s_%s' % (prefix, routine)]
except KeyError:
cxlog.err('AIECore._%s_%s does not exist!' % (cxlog.to_str(prefix), cxlog.to_str(routine)))
continue
cxlog.log_("aie", " running " + cxlog.to_str(routine))
if not func(self):
cxlog.log_("aie", " -> returned False")
if not self.error:
cxlog.err(cxlog.to_str(routine) + ' did not set self.error')
self.error = _("An unknown error occurred in %s") % routine
return False
del self.state['routine_iterator']
return True
#####
#
# Usage logging
#
#####
def log_usage(self):
if self.install_failure_detected():
fail = 1
else:
fail = 0
log_str = "install %s %d %d\n" % (self.installer.parent.appid, self.canceled, fail)
cxulog.log_usage(log_str)
#####
#
# Prepare
#
#####
def _prep_all_fonts(self):
cxlog.log_('aie', 'appid=' + cxlog.to_str(self.installer.parent.appid))
if self.installer.parent.appid == 'com.codeweavers.c4.6959':
for font in self.dependencies:
if isinstance(font, AIECore):
font.state['silentfontinstall'] = True
cxlog.log_('aie', 'set silentfontinstall on ' + cxlog.to_str(font.name))
return True
def _prep_collate_skips(self):
engine = self.scheduler
if 'skipassoc' not in engine.state:
engine.state['skipassoc'] = True
engine.state['skipmenu'] = True
engine.state['installnsplugins'] = False
# These are only set when the corresponding task succeeds though
engine.state['defaulteassocs'] = set()
engine.state['alteassocs'] = set()
engine.state['skipassoc'] &= self.installer.skip_assoc_creation
engine.state['skipmenu'] &= self.installer.skip_menu_creation
engine.state['installnsplugins'] |= self.installer.install_nsplugins
return True
def _prep_collate_nsplugins(self):
engine = self.scheduler
if 'pre_nsplugins' not in engine.state:
engine.state['pre_nsplugins'] = set()
# This one is only set when the corresponding task is run though
engine.state['ignore_nsplugins'] = set()
engine.state['pre_nsplugins'].update(self.installer.pre_install_nsplugin_dlls)
cxlog.log_("aie", "collate_nsplugins %s" % cxlog.to_str(engine.state['pre_nsplugins']))
return True
def prepare(self):
"""This method is called by the install engine after all the
AIECore tasks have been created. The calls are made on children before
the tasks that depend on them. They are not allowed to fail.
"""
routines = ('all_fonts',
'collate_skips',
'collate_nsplugins',
)
return self.run_routines('prep', routines)
#####
#
# Main
#
#####
def _main_setup_drive_letter(self):
"""Create a drive letter for the install source if needed."""
install_source = self.state.get('install_source')
if install_source and os.path.isdir(install_source):
drive_letter = bottlequery.add_drive(self.scheduler.installtask.bottlename, install_source)
if drive_letter:
self.state['temporary_drive'] = drive_letter
return True
def _cleanup_delete_drive_letter(self):
"""Delete the temporary drive letter if we created one."""
if 'temporary_drive' in self.state:
bottlequery.rm_drive(self.scheduler.installtask.bottlename, self.state['temporary_drive'])
del self.state['temporary_drive']
return True
def _main_setup_winver(self):
"""Temporarily change the windows version of the bottle if needed."""
if self.installer.installer_winver:
installer_winver = bottlequery.version_nicknames.get(self.installer.installer_winver, self.installer.installer_winver)
bottlename = self.scheduler.installtask.bottlename
orig_winver = bottlequery.get_windows_version(bottlename)
if installer_winver != orig_winver:
self.state['orig_winver'] = orig_winver
bottlequery.set_windows_version(bottlename, installer_winver)
return True
def _cleanup_reset_winver(self):
"""Restore the windows version in this bottle if we changed it."""
if 'orig_winver' in self.state:
bottlequery.set_windows_version(self.scheduler.installtask.bottlename, self.state['orig_winver'])
del self.state['orig_winver']
return True
def _main_set_installer_file(self):
"""Check that we have an installation source and initialize
installer_file as appropriate.
"""
install_source = self.state.get('install_source')
if 'virtual' in self.installer.parent.app_profile.flags:
# Virtual profiles are not supposed to have an installation source
if install_source:
cxlog.warn("install_source = %s for the %s virtual profile!" % (cxlog.debug_str(install_source), cxlog.debug_str(self.name)))
del self.state['install_source']
return True
if not install_source:
self.error = _("No installation source was provided for %s") % self.name
return False
if os.path.isfile(install_source):
self.state['installer_file'] = install_source
self.state['install_source'] = os.path.dirname(install_source)
elif install_source.lower().startswith("steam:"):
self.state['installer_file'] = install_source
self.state['install_source'] = install_source
elif not os.path.isdir(install_source):
self.error = _("The '%(source)s' installation source of %(name)s is neither a file nor a directory!") % {'source': install_source, 'name': self.name}
return False
return True
def _main_setup_environment(self):
"""Set up the Unix environment to be used by this task's child
processes.
"""
env = self.state['environ'] = self.scheduler.state['environ'].copy()
for envvar in self.installer.installer_environment:
env[envvar.name] = self.scheduler.expand_win_string(envvar.value)
# Defer the menu creation until we run cxmenu --sync
env['CX_NO_WINESHELLLINK'] = '1'
# Prevent the application installers from scanning all of the user's
# hard drive.
env['CX_HACK_REMOTE_DRIVES'] = env.get('CX_HACK_REMOTE_DRIVES', 'yz')
if self.state.get('install_source', '').lower().startswith('steam:'):
env['WINE_WAIT_CHILD_PIPE_IGNORE'] = "steam.exe"
if self.installer.parent.appid == 'com.codeweavers.c4.206' or \
self.state.get('install_source', '').lower().startswith('steam:'):
# Do not ever start a steam process with CX_NO_WINESHELLLINK
# because steam makes its own menus independent of our install logic.
del env['CX_NO_WINESHELLLINK']
return True
def _main_setup_registry_pre(self):
"""Tweak the windows registry for child processes.
Do it early enough so that it's ready self-extracting installers.
"""
for regentry in self.installer.pre_install_registry:
for regvalue in regentry.values:
if not bottlequery.set_registry_key(self.scheduler.installtask.bottlename, regentry.key, regvalue.name, regvalue.data):
return False
return True
def _main_uncompress(self):
"""Check if the installer file is an archive.
If so uncompress it into a temporary directory and arrange for it to
be used as the install_source in the following steps. This installers
can be compressed and it's transparent to the later steps. Also arrange
for the temporary directory to be deleted at the end.
"""
if 'install_source' not in self.state:
# Nothing to do for virtual packages
return True
installer_file = self.state.get('installer_file')
if installer_file is None:
# Nothing to uncompress. We are done here.
return True
archive_type = None
if self.installer.installer_treatas:
lower = self.installer.installer_treatas.lower()
else:
lower = installer_file.lower()
if lower.endswith('.zip'):
archive_type = 'zip'
elif lower.endswith('.cab'):
archive_type = 'cab'
elif lower.endswith('.tgz'):
archive_type = 'tar'
elif lower.endswith('.tar.gz'):
archive_type = 'tar'
elif lower.endswith('.tar.bz2'):
archive_type = 'tar'
elif lower.endswith('.tar'):
archive_type = 'tar'
elif lower.endswith('.tbz'):
archive_type = 'tar'
elif lower.endswith('.tb2'):
archive_type = 'tar'
elif lower.endswith('.rar'):
archive_type = 'rar'
elif lower.endswith('.7z'):
archive_type = '7z'
elif self.installer.selfextract_threshold is not None:
try:
if os.path.getsize(installer_file) >= self.installer.selfextract_threshold * 1024:
archive_type = 'selfextract'
except OSError, ose:
cxlog.log_("aie", cxlog.to_str(ose))
if archive_type is None:
# Not an archive. We are done here.
return True
# Get a temporary directory to extract into
wintmpdir = self.scheduler.expand_win_string("%temp%")
tmpdir = bottlequery.get_native_path(self.scheduler.installtask.bottlename, wintmpdir)
if not tmpdir:
self.error = _("Unable to get a temporary directory for %s") % self.name
return False
tmpdir = tempfile.mkdtemp(dir=tmpdir)
self.state['temporary_source'] = tmpdir
# save the cwd so that we can restore it later on, then
# point at the tempdir in case any of our archive models
# extract directly to the cwd.
previous_cwd = os.getcwd()
os.chdir(tmpdir)
# Zip file support
if archive_type == 'zip':
zipf = zipfile.ZipFile(installer_file, 'r', allowZip64=True)
for source in zipf.namelist():
if source.startswith('/') or '..' in source.split('/'):
# These would be created outside the given path
cxlog.warn("Skipping file %s in %s (bad filename)" % (cxlog.debug_str(source), cxlog.debug_str(installer_file)))
continue
if self.installer.zip_encoding:
target = os.path.join(tmpdir, source.decode(self.installer.zip_encoding))
else:
target = os.path.join(tmpdir, source)
try:
os.makedirs(target.rsplit('/', 1)[0])
except OSError:
# File already exists
pass
if not target.endswith('\\') and not target.endswith('/'):
if self.installer.zip_encoding:
data = zipf.read(source)
dfile = open(target, 'wb')
dfile.write(data)
dfile.close()
else:
try:
# With extract(), introduced in Python 2.6, we don't
# have to read the whole archive member into memory
# which helps a lot for large files.
# pylint: disable=E1101
zipf.extract(source, tmpdir)
except AttributeError:
data = zipf.read(source)
dfile = open(target, 'wb')
dfile.write(data)
dfile.close()
zipf.close()
# Microsoft Cabinet file support
if archive_type == 'cab':
engine = self.scheduler
# pylint: disable=W0632
win_installer_file, win_extractdir = bottlequery.get_windows_paths(
engine.installtask.bottlename,
(installer_file, tmpdir))
cmd = [os.path.join(cxutils.CX_ROOT, "bin", "wine"),
"--bottle", engine.installtask.bottlename, "--no-convert",
"--wait-children", "--wl-app", "extract.exe", "--",
"/E", "/L", win_extractdir, "/R", win_installer_file]
# FIXME: Make interruptible.
retcode, _out, err = cxutils.run(cmd, env=engine.state['environ'],
stderr=cxutils.GRAB)
if retcode:
self.error = err
os.chdir(previous_cwd)
return False
# Tar file support
if archive_type == 'tar':
cxlog.log_('tar', "Extracting archive %s to %s" % (cxlog.debug_str(installer_file), cxlog.debug_str(tmpdir)))
tarf = tarfile.open(installer_file, 'r')
members = tarf.getmembers()
# Extract deeper files first to avoid the issues described at
# http://www.python.org/doc/2.5.2/lib/tarfile-objects.html#l2h-2413
members = sorted(members, key=tar_member_depth, reverse=True)
for member in members:
if member.name.startswith('/') or '..' in member.name.split('/'):
# These would be created outside the given path
cxlog.warn("Skipping file %s in %s (bad filename)" % (cxlog.debug_str(member.name), cxlog.debug_str(installer_file)))
continue
# Make sure everything leading up to and including this file is a directory
path_elements = os.path.normpath(member.name).split('/')
bad_path = False
for i in range(len(path_elements)):
path = os.path.join(tmpdir, '/'.join(path_elements[0:i+1]))
try:
mode = os.lstat(path).st_mode # use lstat because symlinks to directories are bad
except OSError:
# Path does not exist; this is fine
break
if not stat.S_ISDIR(mode):
bad_path = True
break
if bad_path:
# This could be a symlink to something outside the extraction directory.
# Even if it's a regular file, we don't expect to overwrite any of those.
cxlog.warn("Skipping file %s in %s (not a directory)" % (cxlog.debug_str(member.name), cxlog.debug_str(installer_file)))
continue
cxlog.log_('tar', "Extracting file %s to %s" % (cxlog.debug_str(member.name), cxlog.debug_str(tmpdir)))
tarf.extract(member, tmpdir)
tarf.close()
# RAR file support
if archive_type == 'rar':
engine = self.scheduler
cmd = [os.path.join(cxutils.CX_ROOT, "bin", "unrar"),
"x", "-idq", installer_file, tmpdir]
# FIXME: Make interruptible.
retcode, _out, err = cxutils.run(cmd, env=engine.state['environ'],
stderr=cxutils.GRAB)
if retcode:
self.error = err
os.chdir(previous_cwd)
return False
# 7z and NSIS (NullSoft Scriptable Install System) file support
if archive_type == '7z':
engine = self.scheduler
cmd = ["7z", "x", "-o" + tmpdir, installer_file]
# FIXME: Make interruptible.
retcode, _out, err = cxutils.run(cmd, env=engine.state['environ'],
stderr=cxutils.GRAB)
if retcode:
self.error = err
os.chdir(previous_cwd)
return False
# Self-extracting executable
if archive_type == 'selfextract':
engine = self.scheduler
win_installer_file = bottlequery.get_windows_path(engine.installtask.bottlename, installer_file)
# Set %ExtractDir% in the 'Windows' environment variable block so
# it is available when building the self-extracting installer
# command. This way it will know where to extract the files.
# Note that we need to put a Windows path there. We cannot rely on
# the wine script doing the conversion for us because the path is
# likely to be embedded in an option like so '/T:%ExtractDir%'.
# Also note that we modify a variable which may be used by the
# other tasks. That's ok because no other task is going to use
# 'winenv' while AIECore is running.
if not wintmpdir.endswith('\\'):
wintmpdir += '\\'
winenv = engine.get_win_environ()
# The key must be lower case
winenv['extractdir'] = wintmpdir + os.path.basename(tmpdir)
cmd = [os.path.join(cxutils.CX_ROOT, "bin", "wine"),
"--no-convert",
"--bottle", engine.installtask.bottlename,
"--wait-children", win_installer_file]
for option in self.installer.selfextract_options:
cmd.append(engine.expand_win_string(option))
if self.state.get('silentinstall'):
for option in self.installer.selfextract_silent_options:
cmd.append(engine.expand_win_string(option))
del winenv['extractdir']
# FIXME: Make interruptible.
retcode, _out, err = cxutils.run(cmd, env=engine.state['environ'],
stderr=cxutils.GRAB)
if retcode:
self.error = err
os.chdir(previous_cwd)
return False
os.chdir(previous_cwd)
self.state['install_source'] = tmpdir
del self.state['installer_file']
return True
def _main_locate_installer(self):
if not cxaiemedia.needs_installer_file(self.installer):
# Virtual and 'copy-only' profiles don't need an installer file
# and should skip the corresponding steps.
self.state['no_installer'] = True
return True
if 'installer_file' in self.state:
# Nothing to do if we already have an installer file
return True
install_source = self.state['install_source']
if os.path.isdir(install_source):
installer_file = cxaiemedia.locate_installer(self.installer, install_source)
if installer_file:
self.state['installer_file'] = installer_file
cxlog.log_("aie", "installer_file = " + cxlog.debug_str(installer_file))
return True
self.error = _("Unable to find an installer for %(name)s in '%(source)s'") % {'name':self.name, 'source':install_source}
return False
def _main_build_install_command(self):
if 'no_installer' in self.state:
return True
if 'installer_command' in self.state:
# Nothing to do if we already have a command
return True
installer_file = self.state['installer_file']
cmd = [os.path.join(cxutils.CX_ROOT, "bin", "wine"),
"--bottle", self.scheduler.installtask.bottlename,
"--untrusted", "--wait-children", '--no-convert', "--new-console"]
if self.installer.installer_dlloverrides:
cmd.extend(['--dll', self.installer.installer_dlloverrides])
if self.installer.installer_treatas:
lower = self.installer.installer_treatas.lower()
else:
lower = installer_file.lower()
# pylint: disable=W0632
win_installer_file, win_autorun_dir = bottlequery.get_windows_paths(
self.scheduler.installtask.bottlename,
(installer_file, os.path.dirname(installer_file)))
if lower.endswith('/autorun.inf'):
cmd.extend(['--wl-app', 'autorun.exe', '--', win_autorun_dir])
elif lower.startswith('steam:'):
cmd.extend(['--start', installer_file])
elif lower.endswith('.exe') or lower.endswith('.com') or lower.endswith('.bat') or lower.endswith('.cmd'):
cmd.append('--')
cmd.append(win_installer_file)
for option in self.installer.installer_options:
cmd.append(self.scheduler.expand_win_string(option))
if self.state.get('silentinstall'):
for option in self.installer.installer_silent_options:
cmd.append(self.scheduler.expand_win_string(option))
elif lower.endswith('.msi'):
cmd.append('--')
cmd.extend(['msiexec.exe', '/i', win_installer_file])
if self.state.get('silentinstall'):
cmd.append('/quiet')
elif lower.endswith('.msp'):
cmd.append('--')
cmd.extend(['msiexec.exe', '/p', win_installer_file])
elif lower.endswith('.otf') or \
lower.endswith('.ttc') or \
lower.endswith('.ttf'):
cmd.remove('--new-console')
cmd.append('--')
cmd.extend(['cxinstallfonts.exe', win_installer_file])
else:
self.error = _("'%(file)s' is of an unknown installer type for %(name)s") % {'file':installer_file, 'name':self.name}
return False
self.state['installer_command'] = cmd
cxlog.log_("aie", "installer_command=" + ' '.join(cxlog.debug_str(x) for x in cmd))
return True
def _main_collate_eassocs(self):
engine = self.scheduler
engine.state['defaulteassocs'].update(self.installer.default_eassocs)
engine.state['alteassocs'].update(self.installer.alt_eassocs)
return True
def _main_collate_nsplugins(self):
engine = self.scheduler
engine.state['ignore_nsplugins'].update(self.installer.ignore_nsplugin_dlls)
return True
def install_failure_detected(self):
profile = self.installer.parent
if not profile.app_profile.installed_key_pattern and \
not profile.app_profile.installed_display_pattern and \
not profile.app_profile.installed_registry_globs and \
not profile.app_profile.installed_file_globs:
if not self._has_printed_install_log_err:
cxlog.log_("aie", "We have no way to detect success or failure for this package. Assuming success, with optimism.")
self._has_printed_install_log_err = True
return False
if self.state.get('installer_file', '').lower().startswith('steam:'):
return False
else:
return not appdetector.is_profile_installed(self.scheduler.installtask.bottlename, profile)
def _main_run_installer_and_check(self):
if 'no_installer' in self.state:
return True
# We cannot trust the installer's return code so we ignore it entirely.
# FIXME: We should try to be interruptible
is_steam_install = self.state.get('installer_file', '').lower().startswith('steam:')
engine = self.scheduler
source = engine.installtask.GetInstallerSource()
if source and os.path.isdir(source):
workdir = source
else:
workdir = bottlequery.get_native_path(engine.installtask.bottlename, engine.expand_win_string("C:/windows/temp"))
cxutils.system(self.state['installer_command'],
env=self.state['environ'], cwd=workdir,
background=is_steam_install)
if self.install_failure_detected():
self.error = _("The installer has exited but %s does not seem to be installed") % self.name
return False
else:
return True
def _check_fake_dll(self, path):
if os.path.exists(path):
sfile = open(path, "rb")
sfile.seek(0x40)
tag = sfile.read(20)
if tag == "Wine placeholder DLL":
return True
return False
def _main_remove_fake_dlls(self):
engine = self.scheduler
for dllfile in self.installer.pre_rm_fake_dlls:
source = bottlequery.get_native_path(engine.installtask.bottlename, engine.expand_win_string(dllfile))
if not os.path.exists(source):
source = engine.expand_win_string("%WinSysDir%")
target = source
source = os.path.join(source, dllfile)
source = bottlequery.get_native_path(engine.installtask.bottlename, source)
target = os.path.join(target, dllfile + ".bak")
target = bottlequery.get_native_path(engine.installtask.bottlename, target)
else:
target = source + ".bak"
if self._check_fake_dll(source):
if os.path.exists(target):
os.remove(target)
shutil.move(source, target)
else:
cxlog.log(" %s is not a fake dll " % (cxlog.debug_str(dllfile)))
return True
def _main_copy_files(self):
if 'install_source' not in self.state:
# Nothing to do for virtual packages
return True
engine = self.scheduler
matches = {}
if self.installer.files_to_copy and self.installer.files_to_copy[0].glob == ':installer:':
copyentry = self.state.get('installer_file')
matches[self.installer.files_to_copy[0]] = [copyentry]
else:
globber = globtree.FileGlobTree()
for copyentry in self.installer.files_to_copy:
globber.add_glob(copyentry.glob, copyentry)
install_source = self.state['install_source']
for match, copyentry in globber.matches(install_source):
if copyentry in matches:
matches[copyentry].append(match)
else:
matches[copyentry] = [match]
destinations = {}
# Preserve the order of the copy entries so the later ones overwrite
# the former ones
for copyentry in self.installer.files_to_copy:
if copyentry not in matches:
continue
sources = matches[copyentry]
cxlog.log_("aie", "* %s -> %s" % (cxlog.debug_str(copyentry.glob), cxlog.debug_str(copyentry.destination)))
# First figure out what the destination is, trying to minimise the
# number of patch conversions
if copyentry.destination in destinations:
destination = destinations[copyentry.destination]
else:
destination = bottlequery.get_native_path(engine.installtask.bottlename, engine.expand_win_string(copyentry.destination))
destinations[copyentry.destination] = destination
# Note that if destination exists, it may have lost its trailing
# '/'. So check the profile intent using copyentry.destination.
# Also the copy operation should be idempotent. This is what
# requires the isdir(sources[0]) check.
if len(sources) == 1 and \
not copyentry.destination.endswith("/") and \
not os.path.isdir(destination) and \
not os.path.isdir(sources[0]):
dst_dir = os.path.dirname(destination)
else:
dst_dir = destination
destination = None
# Then create the appropriate destination directory
try:
os.makedirs(dst_dir)
except OSError, ose:
if ose.errno != errno.EEXIST:
self.error = _("Unable to create the destination directory '%(dir)s' for '%(profile)s':\n%(error)s") % {
'dir': dst_dir,
'profile': self.name,
'error': unicode(ose)}
return False
if not os.path.isdir(dst_dir):
self.error = _("Cannot copy the %(profile)s file(s) into '%(dir)s' because it is not a directory.") % {
'profile': self.name,
'dir': dst_dir}
return False
# Finally, do the actual copying
for source in sources:
dst = destination
if not dst:
dst = os.path.join(dst_dir, os.path.basename(source))
try:
cxlog.log_("aie", "copying %s to '%s'" % (cxlog.debug_str(source), cxlog.debug_str(dst)))
if os.path.isfile(source):
shutil.copy2(source, dst)
else:
# FIXME: Should also work if the destination exists
shutil.copytree(source, dst)
except OSError, ose:
self.error = _("An error occurred while copying '%(source)s' to '%(destination)s':\n%(error)s") % {
'source': source,
'destination': dst,
'error': unicode(ose)}
return False
return True
def _main_reboot(self):
if not self.installer.post_install_reboot:
return True
engine = self.scheduler
cmd = [os.path.join(cxutils.CX_ROOT, "bin", "wine"),
"--bottle", engine.installtask.bottlename,
"--wait-children", "--wl-app", "reboot.exe"]
# FIXME: Make interruptible.
retcode, _out, err = cxutils.run(cmd, env=engine.state['environ'],
stderr=cxutils.GRAB)
if retcode:
self.error = err
return False
return True
def _main_create_lnk_files(self):
engine = self.scheduler
for lnk in self.installer.lnk_files:
cmd = [os.path.join(cxutils.CX_ROOT, "bin", "wine"),
"--bottle", engine.installtask.bottlename,
"--wait-children", "--wl-app", "cxmklnk.exe", "--",
"--lnkfile", engine.expand_win_string(lnk.shortcut) + ".lnk",
"--target", engine.expand_win_string(lnk.target)]
if lnk.workdir:
cmd.extend(("--workdir", engine.expand_win_string(lnk.workdir)))
# FIXME: Make interruptible.
retcode, _out, err = cxutils.run(cmd, env=engine.state['environ'],
stderr=cxutils.GRAB)
if retcode:
self.error = err
return False
return True
def _main_apply_menu_hints(self):
if not self.installer.skip_menu_creation:
engine = self.scheduler
cmd = [os.path.join(cxutils.CX_ROOT, "bin", "cxmenu"),
"--bottle", engine.installtask.bottlename,
"--sync", "--mode", "install"]
if self.installer.mainmenu_never:
cmd.append("--mmenu-never")
cmd.append(':'.join(self.installer.mainmenu_never))
retcode, _out, err = cxutils.run(cmd, env=engine.state['environ'],
stderr=cxutils.GRAB)
if retcode:
self.error = err
return False
return True
def _main_register_dlls(self):
engine = self.scheduler
for dll in self.installer.post_registerdll:
expanded_dll = engine.expand_win_string(dll)
cmd = [os.path.join(cxutils.CX_ROOT, "bin", "wine"),
"--bottle", engine.installtask.bottlename,
"--wait-children", "--no-convert", "--wl-app", "regsvr32.exe",
"--", "/s", expanded_dll]
workdir = None
if ('/' in expanded_dll) or ('\\' in expanded_dll):
path = bottlequery.get_native_path(engine.installtask.bottlename, expanded_dll)
basedir = os.path.dirname(path)
if os.path.isdir(basedir):
workdir = basedir
# FIXME: Make interruptible.
retcode, _out, err = cxutils.run(cmd, env=engine.state['environ'],
stderr=cxutils.GRAB, cwd=workdir)
if retcode:
self.error = err
return False
return True
def _cleanup_delete_uncompressed_files(self):
if 'temporary_source' in self.state:
shutil.rmtree(self.state['temporary_source'])
del self.state['temporary_source']
return True
def _main_check_silent_steam_install(self):
# If Steam is being installed as a dependency, make
# the install silent. That allows the installer to return
# immediately so we can get on with our business.
if self.installer.parent.appid == 'com.codeweavers.c4.206':
for parent in self.parents:
if type(parent).__name__ == 'AIECore':
self.state['silentinstall'] = True
break
return True
# The routines implementing the 'All Fonts' hack
def _main_check_silent_font_install(self):
cxlog.log_('aie', 'appid=%s silentfontinstall=%s global=%s' % (cxlog.to_str(self.installer.parent.appid), cxlog.to_str(self.state.get('silentfontinstall')), cxlog.to_str(self.scheduler.state.get('silentfontinstall'))))
if self.state.get('silentfontinstall') and self.scheduler.state.get('silentfontinstall'):
self.state['silentinstall'] = True
cxlog.log_('aie', ' -> silentinstall = True')
return True
def _main_set_silent_font_install(self):
cxlog.log_('aie', 'appid=%s' % cxlog.to_str(self.installer.parent.appid))
if self.state.get('silentfontinstall'):
cxlog.log_('aie', ' -> global silentfontinstall = True')
self.scheduler.state['silentfontinstall'] = True
return True
def _main_cxhack_steam(self):
# custom hacks for com.codeweavers.c4.206 aka Steam
# occurs after install finishes on non-macs
if not distversion.IS_MACOSX:
if self.installer.parent.appid == 'com.codeweavers.c4.206':
engine = self.scheduler
target = os.path.join(engine.expand_win_string("%ProgramFiles%"), "Steam")
target = bottlequery.get_native_path(engine.installtask.bottlename, target)
if target:
cmd = ["chattr", "-R", "-D", target]
cxutils.run(cmd, env=engine.state['environ'], stderr=cxutils.GRAB)
return True
def _main_cxhack_dcom98_pre(self):
if self.installer.parent.appid == 'com.codeweavers.c4.6936':
# We need to move stdole32.tbl out of the way.
engine = self.scheduler
source = os.path.join(engine.expand_win_string("%WinDir%"), "system32", "stdole32.tlb")
target = os.path.join(engine.expand_win_string("%WinDir%"), "system32", "stdole32.tlb.bak")
source = bottlequery.get_native_path(engine.installtask.bottlename, source)
target = bottlequery.get_native_path(engine.installtask.bottlename, target)
if os.path.exists(source):
if os.path.exists(target):
os.remove(target)
shutil.move(source, target)
return True
def _main_cxhack_ie6_pre(self):
# custom hacks for com.codeweavers.c4.15 aka IE6
# occurs before install
# also applies to Office XP (1544), which will install IE5 in win98 mode
if self.installer.parent.appid == 'com.codeweavers.c4.15' or \
self.installer.parent.appid == 'com.codeweavers.c4.7853' or \
self.installer.parent.appid == 'com.codeweavers.c4.8054' or \
(self.installer.parent.appid == 'com.codeweavers.c4.1544' and
self.scheduler.installtask.target_template == 'win98'):
engine = self.scheduler
cmd = [os.path.join(cxutils.CX_ROOT, "bin", "wine"),
"--bottle", engine.installtask.bottlename,
"--wait-children", "--dll", "advpack=b",
"--wl-app", "iexplore.exe", "-unregserver"]
retcode, _out, err = cxutils.run(cmd, env=engine.state['environ'],
stderr=cxutils.GRAB)
if retcode:
self.error = err
return False
# move the builtin iexplore.exe out of the way
source = os.path.join(engine.expand_win_string("%ProgramFiles%"), "Internet Explorer")
target = source
source = os.path.join(source, "iexplore.exe")
source = bottlequery.get_native_path(engine.installtask.bottlename, source)
target = os.path.join(target, "iexplore.exe.bak")
target = bottlequery.get_native_path(engine.installtask.bottlename, target)
if os.path.exists(source):
if os.path.exists(target):
os.remove(target)
shutil.move(source, target)
# If we aren't on win98, we're going to need to extract
# inseng.dll. On win98 this change isn't needed.
# The extraction doesn't work with some older versions of IE,
# but fortunately those versions are all installed on win98 anyway.
if self.scheduler.installtask.target_template != 'win98':
installer_file = self.state['installer_file']
wintmpdir = self.scheduler.expand_win_string("%temp%")
cwintmpdir = bottlequery.get_native_path(engine.installtask.bottlename, wintmpdir)
tmpdir = tempfile.mkdtemp(dir=cwintmpdir)
wintmpdir = bottlequery.get_windows_path(engine.installtask.bottlename, tmpdir)
win_installer_file = bottlequery.get_windows_path(engine.installtask.bottlename, installer_file)
cmd = [os.path.join(cxutils.CX_ROOT, "bin", "wine"),
"--bottle", engine.installtask.bottlename, "--no-convert",
"--wait-children", "--", win_installer_file, "/T:"+wintmpdir,
"/C"]
retcode, _out, err = cxutils.run(cmd, env=engine.state['environ'],
stderr=cxutils.GRAB)
if retcode:
cxlog.err('Failed to extract dlls from the IE6 installed.\n')
self.error = err
return False
source = os.path.join(tmpdir, "inseng.dll")
target = os.path.join(engine.expand_win_string("%WinDir%"), "system32", "inseng.dll")
target = bottlequery.get_native_path(engine.installtask.bottlename, target)
if os.path.exists(source):
if os.path.exists(target):
os.remove(target)
shutil.move(source, target)
shutil.rmtree(tmpdir)
return True
def _main_cxhack_ie6_post(self):
# custom hacks for com.codeweavers.c4.15 aka IE6
# occurs after install
# also applies to Office XP (1544), which will install IE5 in win98 mode
if self.installer.parent.appid == 'com.codeweavers.c4.15' or \
self.installer.parent.appid == 'com.codeweavers.c4.8054' or \
(self.installer.parent.appid == 'com.codeweavers.c4.1544' and
self.scheduler.installtask.target_template == 'win98'):
engine = self.scheduler
# if iexplore.exe does not exist restore our backup
source = os.path.join(engine.expand_win_string("%ProgramFiles%"), "Internet Explorer")
target = source
target = os.path.join(source, "iexplore.exe")
target = bottlequery.get_native_path(engine.installtask.bottlename, target)
if not globtree.file_exists_insensitive(target):
source = os.path.join(source, "iexplore.exe.bak")
source = bottlequery.get_native_path(engine.installtask.bottlename, source)
if os.path.exists(source):
shutil.move(source, target)
return True
def _main_cxhack_mplayer2(self):
# custom hacks for com.codeweavers.c4.96 aka Windows Media Player 6.4
# occurs before install
if self.installer.parent.appid == 'com.codeweavers.c4.96':
engine = self.scheduler
# remove wininit.ini
target = os.path.join(engine.expand_win_string("%WinDir%"), "wininit.ini")
target = bottlequery.get_native_path(engine.installtask.bottlename, target)
if os.path.exists(target):
os.remove(target)
return True
def _main_cxhack_cxhtml(self):
# custom hacks for com.codeweavers.c4.cxhtml
# occurs before install
if self.installer.parent.appid == 'com.codeweavers.c4.cxhtml' or \
self.installer.parent.appid == 'com.codeweavers.c4.7828':
engine = self.scheduler
cmd = [os.path.join(cxutils.CX_ROOT, "bin", "wine"),
"--bottle", engine.installtask.bottlename,
"--wait-children", "--wl-app", "iexplore.exe", "-regserver"]
retcode, _out, err = cxutils.run(cmd, env=engine.state['environ'],
stderr=cxutils.GRAB)
if retcode:
self.error = err
return False
return True
def _main_cxhack_dotnet(self):
# dotnet needs to whip out the wine version before it installs
# custom hacks for com.codeweavers.c4.597
# occurs before install
if self.installer.parent.appid == 'com.codeweavers.c4.597' or \
self.installer.parent.appid == 'com.codeweavers.c4.1578' or \
self.installer.parent.appid == 'com.codeweavers.c4.5917':
engine = self.scheduler
cmd = [os.path.join(cxutils.CX_ROOT, "bin", "wine"),
"--bottle", engine.installtask.bottlename,
"uninstaller", "--remove",
"{E45D8920-A758-4088-B6C6-31DBB276992E}"]
_retcode, _out, _err = cxutils.run(cmd, env=engine.state['environ'],
stderr=cxutils.GRAB)
return True
def _main_cxhack_dotnet30(self):
# dotnet 3.0 and 3.0 sp 1 needs to remove some service keys
# custom hacks for com.codeweavers.c4.9383 and 3356
# occurs before install
if self.installer.parent.appid == 'com.codeweavers.c4.9383' or \
self.installer.parent.appid == 'com.codeweavers.c4.3356':
engine = self.scheduler
cmd = [os.path.join(cxutils.CX_ROOT, "bin", "wine"),
"--bottle", engine.installtask.bottlename,
"--wait-children", "--wl-app", "sc.exe",
"delete", "FontCache3.0.0.0"]
_retcode, _out, _err = cxutils.run(cmd, env=engine.state['environ'],
stderr=cxutils.GRAB)
return True
def _main_cxhack_dotnet20sp(self):
# dotnet 2.0 sps needs to make sure a service is shut down
# custom hacks for com.codeweavers.c4.9322 and 6945
# occurs before install
if self.installer.parent.appid == 'com.codeweavers.c4.9322' or \
self.installer.parent.appid == 'com.codeweavers.c4.6945':
engine = self.scheduler
cmd = [os.path.join(cxutils.CX_ROOT, "bin", "wine"),
"--bottle", engine.installtask.bottlename,
"--wl-app", "sc", "stop",
"clr_optimization_v2.0.50727_32"]
cxutils.run(cmd, env=engine.state['environ'])
# We do not want to report failure here as if the service
# does not exist that is just fine also.
return True
def _cleanup_cxhack_dotnet20sp(self):
# dotnet 2.0 sps needs to make sure everything is shut down at the end
# custom hacks for com.codeweavers.c4.9322 and 6945
# occurs before install
if self.installer.parent.appid == 'com.codeweavers.c4.9322' or \
self.installer.parent.appid == 'com.codeweavers.c4.6945':
engine = self.scheduler
cmd = [os.path.join(cxutils.CX_ROOT, "bin", "wine"),
"--bottle", engine.installtask.bottlename,
"--wl-app", "wineboot", "--", "--end-session",
"--shutdown", "--force", "--kill"]
retcode, _out, err = cxutils.run(cmd, env=engine.state['environ'],
stderr=cxutils.GRAB)
if retcode:
self.error = err
return False
bottlequery.shutdown_manipulator(engine.installtask.bottlename)
return True
def _cleanup_cxhack_taskkill(self):
# custom hacks for com.codeweavers.c4.8882 aka TaskKill
# occurs after install
if self.installer.parent.appid == 'com.codeweavers.c4.8882':
engine = self.scheduler
cmd = [os.path.join(cxutils.CX_ROOT, "bin", "wine"),
"--bottle", engine.installtask.bottlename,
"--wl-app", "wineboot", "--", "--end-session",
"--shutdown", "--force", "--kill"]
retcode, _out, err = cxutils.run(cmd, env=engine.state['environ'],
stderr=cxutils.GRAB)
if retcode:
self.error = err
return False
bottlequery.shutdown_manipulator(engine.installtask.bottlename)
return True
def _cleanup_delete_downloaded_installer(self):
# Deletes the downloaded installer file if <alwaysredownload> is true
temp_install_source = self.state.get('temp_install_source')
if temp_install_source:
os.remove(temp_install_source)
return True
def _cleanup(self):
result = True
routines = ('reset_winver',
'delete_drive_letter',
'delete_uncompressed_files',
'cxhack_dotnet20sp',
'cxhack_taskkill',
'delete_downloaded_installer',
)
self.cleanup_lock.acquire() # If we run cleanup functions concurrently, they could return too early
try:
for routine in routines:
# Don't use run_routines for this because we always want to run all of them.
try:
if not getattr(self, '_cleanup_%s' % routine)():
result = False
except:
cxlog.warn("Cleanup function failed:\n%s" % traceback.format_exc())
result = False
finally:
self.cleanup_lock.release()
return result
def can_cancel(self):
return True
def cancel(self):
cxaiebase.AIETask.cancel(self)
# We can't interrupt an installer in progress, but we can stop the process.
self.canceled = True
self._cleanup()
def main(self):
routines = ('setup_drive_letter',
'set_installer_file',
'check_silent_font_install',
'check_silent_steam_install',
'setup_environment',
'cxhack_dotnet',
'setup_winver',
'setup_registry_pre',
'uncompress',
'locate_installer',
'build_install_command',
'collate_eassocs',
'collate_nsplugins',
'cxhack_ie6_pre',
'cxhack_dcom98_pre',
'cxhack_mplayer2',
'cxhack_cxhtml',
'cxhack_dotnet30',
'remove_fake_dlls',
'cxhack_dotnet20sp',
'run_installer_and_check',
'copy_files',
'reboot',
'create_lnk_files',
'apply_menu_hints',
'register_dlls',
'set_silent_font_install',
'cxhack_steam',
'cxhack_ie6_post',
)
try:
main_success = self.run_routines('main', routines)
finally:
cleanup_success = self._cleanup()
return main_success and cleanup_success