# (c) Copyright 2010-2011. CodeWeavers, Inc.
import os.path
import sys
import re
import cxconfig
import cxobservable
import cxutils
import distversion
#####
#
# The current product id
#
#####
_PRODUCT_ID = None
def get_product_id():
"""Gets and checks the effective product id.
An invalid product id is a fatal error and will raise an exception.
"""
# pylint: disable=W0603
global _PRODUCT_ID
if _PRODUCT_ID is None:
filename = os.path.join(cxutils.CX_ROOT, ".productid")
if os.path.exists(filename):
try:
thisfile = open(filename, 'r')
_PRODUCT_ID = thisfile.readline().rstrip()
thisfile.close()
except OSError:
ose = sys.exc_info()[1]
raise RuntimeError("unable to open, read or close '%s': %s" % (filename, unicode(ose)))
else:
_PRODUCT_ID = distversion.BUILTIN_PRODUCT_ID
if len(_PRODUCT_ID) < 4:
raise RuntimeError("product id '%s' is too short" % _PRODUCT_ID)
if not re.search("^[a-zA-Z0-9_][a-zA-Z0-9_][a-zA-Z0-9_][a-zA-Z0-9_][a-zA-Z0-9_]*$", _PRODUCT_ID):
raise RuntimeError("product id '%s' contains bad characters" % _PRODUCT_ID)
return _PRODUCT_ID
#####
#
# CrossOver's important directories
#
#####
def get_user_dir():
try:
home = os.environ["HOME"]
stat = os.stat(home)
euid = os.geteuid()
if euid == 0 and stat.st_uid != euid:
raise RuntimeError("$HOME (%s) does not belong to root" % home)
except KeyError:
raise RuntimeError("$HOME is not set!")
except OSError:
import errno
ose = sys.exc_info()[1]
if ose.errno == errno.ENOENT:
raise RuntimeError("$HOME (%s) does not exist!" % home)
raise
if distversion.IS_MACOSX:
return cxutils.string_to_unicode(os.path.join(home, "Library/Application Support/", distversion.PRODUCT_NAME))
return cxutils.string_to_unicode(os.path.join(home, "." + get_product_id()))
def get_games_dir():
try:
home = os.environ["HOME"]
stat = os.stat(home)
euid = os.geteuid()
if euid == 0 and stat.st_uid != euid:
raise RuntimeError("$HOME (%s) does not belong to root" % home)
except KeyError:
raise RuntimeError("$HOME is not set!")
except OSError:
import errno
ose = sys.exc_info()[1]
if ose.errno == errno.ENOENT:
raise RuntimeError("$HOME (%s) does not exist!" % home)
raise
if distversion.IS_MACOSX:
return unicode(os.path.join(home, "Library/Application Support/", "CrossOver Games"))
return unicode(os.path.join(home, "." + "cxgames"))
def get_managed_dir():
if distversion.IS_MACOSX:
return unicode(os.path.join(("/Library/Application Support"), distversion.PRODUCT_NAME))
return unicode(os.path.join(cxutils.CX_ROOT, "support"))
def is_root_install():
"""Returns True if this install is owned by root.
This is intended to distinguish between system-wide and private installs.
"""
stat = os.stat(cxutils.CX_ROOT)
return stat.st_uid == 0
def is_64bit_install():
if not os.path.exists(os.path.join(cxutils.CX_ROOT, 'lib64', 'wine')):
return False
try:
return os.readlink(os.path.join(cxutils.CX_ROOT, 'bin', 'wineserver')) == 'wineserver64'
except OSError:
# not a symlink
return True
#####
#
# CrossOver's configuration
#
#####
_CONFIG = None
def get_config():
"""Returns the CrossOver configuration, merged from the relevant sources
as needed.
To avoid races when changing the configuration file it is necessary to
follow these rules:
- The data obtained prior to locking the writable configuration file with
must not be trusted. This is because the configuration file was not yet
locked and thus may have changed between the two calls.
- So the proper way to test the value of a setting and then modify it
race-free is:
config = cxproduct.get_config()
# This returns the file the changes should be saved into
wconfig = config.get_save_config()
# This locks the file and re-reads it if needed, ensuring wconfig
# *and* config see the very latest data.
wconfig.lock_file()
if config['section']['field'] ...:
wconfig['section']['field'] = ...
# Saves the changes. If nothing was changed above then the file is
# left untouched. Then the file is unlocked.
wconfig.save_and_unlock_file()
All the while config can still be used in a read-only fashion and is
guaranteed to be in sync with the latest modifications made in
wconfig.
"""
# pylint: disable=W0603
global _CONFIG
if _CONFIG is None:
_CONFIG = cxconfig.Stack()
basename = get_product_id() + ".conf"
global_config_file = os.path.join(cxutils.CX_ROOT, 'etc', basename)
_CONFIG.addconfig(global_config_file)
try:
user_config_file = os.path.join(get_user_dir(), basename)
_CONFIG.addconfig(user_config_file)
except KeyError:
# $HOME is not set, ignore it.
pass
except RuntimeError:
# $HOME is wrong, ignore it.
pass
return _CONFIG
def save_setting(section, field, value):
"""Saves the specified setting in the CrossOver configuration file.
This function must not be used if value is based on another configuration
setting as this would have races.
"""
config = get_config()
wconfig = config.get_save_config()
wconfig.lock_file()
wconfig[section][field] = value
wconfig.save_and_unlock_file()
#####
#
# CrossOver's environment settings
#
#####
_ENVIRON = None
def _refresh_environment(config, *_args):
"""Refreshes the block holding CrossOver's environment settings."""
# Variables can refer to environment variables
environ = os.environ.copy()
# And a few CrossOver-specific variables
if 'CX_ROOT' not in environ:
environ['CX_ROOT'] = cxutils.CX_ROOT
if 'CX_BOTTLE_PATH' not in environ:
if distversion.IS_MACOSX:
environ['CX_BOTTLE_PATH'] = os.path.join(get_user_dir(), "Bottles")
else:
environ['CX_BOTTLE_PATH'] = get_user_dir()
if 'CX_MANAGED_BOTTLE_PATH' not in environ:
if distversion.IS_MACOSX:
environ['CX_MANAGED_BOTTLE_PATH'] = os.path.join(get_managed_dir(), "Bottles")
else:
environ['CX_MANAGED_BOTTLE_PATH'] = get_managed_dir()
if 'CX_GAMES_BOTTLE_PATH' not in environ:
if distversion.IS_MACOSX:
environ['CX_GAMES_BOTTLE_PATH'] = os.path.join(get_games_dir(), "Bottles")
else:
environ['CX_GAMES_BOTTLE_PATH'] = get_games_dir()
if config is None:
config = get_config()
# Note that adding ourselves as an observer means we will be called
# from multiple threads. No locking is needed however.
config.add_observer(cxobservable.ANY_EVENT, _refresh_environment)
# Process each configuration file individually and in the right order in
# case the more specific ones build on the more general ones.
configs = config.configs()
for conffile in reversed(configs):
section = conffile["EnvironmentVariables"]
# Then process the environment variables in the order in which they
# appear in the file in case they use environment variables defined
# before.
for key in section.sortedkeys():
name = section.fieldname(key)
environ[name] = cxutils.expand_unix_string(environ, section[key])
# pylint: disable=W0603
global _ENVIRON
_ENVIRON = environ
def get_bottle_path():
if _ENVIRON is None:
_refresh_environment(None)
return _ENVIRON['CX_BOTTLE_PATH']
def get_managed_bottle_path():
if _ENVIRON is None:
_refresh_environment(None)
return _ENVIRON['CX_MANAGED_BOTTLE_PATH']
def get_games_bottle_path():
if _ENVIRON is None:
_refresh_environment(None)
return _ENVIRON['CX_GAMES_BOTTLE_PATH']
#####
#
# Inter-product configuration (currently only used on Unix)
#
#####
_CROSS_PRODUCT_CONFIG = None
def get_cross_product_config():
"""Returns a configuration object containing the registered CrossOver
product."""
# pylint: disable=W0603
global _CROSS_PRODUCT_CONFIG
if _CROSS_PRODUCT_CONFIG is None:
_CROSS_PRODUCT_CONFIG = cxconfig.Stack()
_CROSS_PRODUCT_CONFIG.addconfig('/etc/crossover.conf')
if 'HOME' in os.environ and (os.geteuid() != 0 or \
cxutils.CX_ROOT.startswith(os.environ['HOME'])):
_CROSS_PRODUCT_CONFIG.addconfig(os.path.join(os.environ['HOME'],
'.crossover.conf'))
return _CROSS_PRODUCT_CONFIG
#####
#
# Product discovery
#
#####
def this_product():
"""Returns a dictionary describing the current product.
See enum_products() for the content of the dictionary."""
result = {}
result['root'] = cxutils.CX_ROOT
result['name'] = distversion.PRODUCT_NAME
result['productid'] = distversion.BUILTIN_PRODUCT_ID
if result['productid'] == 'CrossOver':
result['productid'] = 'cxoffice'
elif result['productid'] == 'CrossOverGames':
result['productid'] = 'cxgames'
result['productversion'] = distversion.CX_VERSION
result['publicversion'] = distversion.PUBLIC_VERSION
return result
def product_info_from_cxroot(root):
"""Returns a dictionary describing the product in the given directory, or
None if it is older than 9.x or not CrossOver.
See enum_products() for the content of the dictionary."""
if not os.path.exists(os.path.join(root, 'bin', 'cxinstaller')):
return None
dist = {}
try:
execfile(os.path.join(root, 'lib', 'python', 'distversion.py'), dist)
except IOError:
return None
for key in ('CX_VERSION', 'PUBLIC_VERSION', 'BUILTIN_PRODUCT_ID', 'PRODUCT_NAME'):
if key not in dist:
return None
product = {}
product['root'] = os.path.abspath(root)
product['name'] = dist['PRODUCT_NAME']
product['productid'] = dist['BUILTIN_PRODUCT_ID']
product['productversion'] = dist['CX_VERSION']
product['publicversion'] = dist['PUBLIC_VERSION']
return product
def enum_products():
"""Returns a dictionary of dictionaries describing the installed products.
The dictionary key is CX_ROOT.
The following values will be present:
* name - Human-readable product name
* productid - The product ID used to test <cxversion> tags.
* productversion - The CrossOver version used to test <cxversion> tags.
* publicversion - The public version.
* root - The value of CX_ROOT."""
# Check the global configuration file.
config = get_cross_product_config()
result = {}
for name, section in config.iteritems():
if name.lower().startswith('product-') and \
os.path.isdir(section.get('Root', '')) and \
'ProductID' in section and 'ProductVersion' in section:
product = {}
product['name'] = section.get('Name', '')
product['root'] = section['Root']
product['productid'] = section['ProductID']
product['productversion'] = section['ProductVersion']
product['publicversion'] = section.get('PublicVersion', '')
result[product['root']] = product
# Add the running product to the list, just in case.
product = this_product()
result[product['root']] = product
# Check in "likely" locations
for root in ('/opt/cxoffice', '/opt/cxgames', os.path.join(os.environ["HOME"], 'cxoffice'), os.path.join(os.environ["HOME"], 'cxgames')):
if root in result:
continue
product = product_info_from_cxroot(root)
if product is not None:
result[product['root']] = product
return result