158 lines
6.8 KiB
Python
158 lines
6.8 KiB
Python
import configparser
|
|
import copy
|
|
import io
|
|
import logging
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
# TODO: time
|
|
_locale_re = re.compile(r'^(?!#\s|)$')
|
|
_locale_def_re = re.compile(r'([^.]*)[^@]*(.*)')
|
|
|
|
|
|
class Locale(object):
|
|
def __init__(self, chroot_base, locales_xml):
|
|
self.xml = locales_xml
|
|
self.chroot_base = chroot_base
|
|
self.syslocales = {}
|
|
self.userlocales = []
|
|
self.rawlocales = None
|
|
self._localevars = configparser.ConfigParser()
|
|
self._localevars.optionxform = str
|
|
self._localevars['BASE'] = {}
|
|
self._initVars()
|
|
|
|
def _initVars(self):
|
|
for l in self.xml.findall('locale'):
|
|
locale = l.text.strip()
|
|
self._localevars['BASE'][l.attrib['name'].strip()] = locale
|
|
if locale not in self.userlocales:
|
|
self.userlocales.append(locale)
|
|
if not self.userlocales:
|
|
self.userlocales = ['en_US', 'en_US.UTF-8']
|
|
_logger.debug('Rendered locales: {0}'.format(dict(self._localevars['BASE'])))
|
|
_logger.debug('Rendered user locales: {0}'.format(','.join(self.userlocales)))
|
|
return(None)
|
|
|
|
def _verify(self):
|
|
localegen = os.path.join(self.chroot_base, 'etc', 'locale.gen') # This *should* be brand new.
|
|
with open(localegen, 'r') as fh:
|
|
self.rawlocales = fh.read().splitlines()
|
|
for idx, line in enumerate(self.rawlocales[:]):
|
|
if _locale_re.search(line) or line.strip() == '':
|
|
continue
|
|
locale, charset = line.split()
|
|
locale = locale.replace('#', '')
|
|
self.syslocales[locale] = charset
|
|
if locale in self.userlocales:
|
|
# "Uncomment" the locale (self.writeConf() actually writes the change)
|
|
self.rawlocales[idx] = '{0} {1}'.format(locale, charset)
|
|
_logger.debug('Rendered system locales: {0}'.format(self.syslocales))
|
|
userl = set(self.userlocales)
|
|
sysl = set(self.syslocales.keys())
|
|
missing_locales = (userl - sysl)
|
|
if (userl - sysl):
|
|
_logger.error('Specified locale(s) {0} that does not exist on the target system.'.format(missing_locales))
|
|
raise ValueError('Missing locale(s)')
|
|
return(None)
|
|
|
|
def writeConf(self):
|
|
# We basically recreate locale-gen in python here, more or less.
|
|
self._verify()
|
|
localegen = os.path.join(self.chroot_base, 'etc', 'locale.gen')
|
|
localedbdir = os.path.join(self.chroot_base, 'usr', 'lib', 'locale')
|
|
localesrcdir = os.path.join(self.chroot_base, 'usr', 'share', 'i18n')
|
|
with open(localegen, 'w') as fh:
|
|
fh.write('# Generated by AIF-NG.\n\n')
|
|
fh.write('\n'.join(self.rawlocales))
|
|
fh.write('\n')
|
|
_logger.info('Wrote: {0}'.format(localegen))
|
|
# If only the locale DB wasn't in a hopelessly binary format.
|
|
# These destinations are built by the below subprocess call.
|
|
for root, dirs, files in os.walk(localedbdir):
|
|
for f in files:
|
|
fpath = os.path.join(root, f)
|
|
os.remove(fpath)
|
|
for d in dirs:
|
|
dpath = os.path.join(root, d)
|
|
shutil.rmtree(dpath)
|
|
_logger.debug('Pruned locale destination.')
|
|
for locale in self.userlocales:
|
|
lpath = os.path.join(localesrcdir, 'locales', locale)
|
|
charset = self.syslocales[locale]
|
|
if os.path.isfile(lpath):
|
|
ldef_name = locale
|
|
else:
|
|
ldef_name = _locale_def_re.sub(r'\g<1>\g<2>', locale)
|
|
lpath = os.path.join(localesrcdir, 'locales', ldef_name)
|
|
env = copy.deepcopy(dict(os.environ))
|
|
env['I18NPATH'] = localesrcdir
|
|
_logger.debug('Invocation environment: {0}'.format(env))
|
|
cmd = subprocess.run(['localedef',
|
|
'--force',
|
|
# These are overridden by a prefix env var.
|
|
# '--inputfile={0}'.format(lpath),
|
|
# '--charmap={0}'.format(os.path.join(localesrcdir, 'charmaps', charset)),
|
|
'--inputfile={0}'.format(ldef_name),
|
|
'--charmap={0}'.format(charset),
|
|
'--alias-file={0}'.format(os.path.join(self.chroot_base,
|
|
'usr', 'share', 'locale', 'locale.alias')),
|
|
'--prefix={0}'.format(self.chroot_base),
|
|
locale],
|
|
stdout = subprocess.PIPE,
|
|
stderr = subprocess.PIPE,
|
|
env = env)
|
|
_logger.info('Executed: {0}'.format(' '.join(cmd.args)))
|
|
if cmd.returncode != 0:
|
|
_logger.warning('Command returned non-zero status')
|
|
_logger.debug('Exit status: {0}'.format(str(cmd.returncode)))
|
|
for a in ('stdout', 'stderr'):
|
|
x = getattr(cmd, a)
|
|
if x:
|
|
_logger.debug('{0}: {1}'.format(a.upper(), x.decode('utf-8').strip()))
|
|
raise RuntimeError('Failed to render locales successfully')
|
|
cfg = os.path.join(self.chroot_base, 'etc', 'locale.conf')
|
|
# And now we write the variables.
|
|
# We have to strip out the section from the ini.
|
|
cfgbuf = io.StringIO()
|
|
self._localevars.write(cfgbuf, space_around_delimiters = False)
|
|
cfgbuf.seek(0, 0)
|
|
with open(cfg, 'w') as fh:
|
|
for line in cfgbuf.readlines():
|
|
if line.startswith('[BASE]') or line.strip() == '':
|
|
continue
|
|
fh.write(line)
|
|
os.chmod(cfg, 0o0644)
|
|
os.chown(cfg, 0, 0)
|
|
_logger.info('Wrote: {0}'.format(cfg))
|
|
return(None)
|
|
|
|
|
|
class Timezone(object):
|
|
def __init__(self, chroot_base, timezone):
|
|
self.tz = timezone.strip().replace('.', '/')
|
|
self.chroot_base = chroot_base
|
|
|
|
def _verify(self):
|
|
tzfilebase = os.path.join('usr', 'share', 'zoneinfo', self.tz)
|
|
tzfile = os.path.join(self.chroot_base, tzfilebase)
|
|
if not os.path.isfile(tzfile):
|
|
_logger.error('Timezone {0} does not have a matching timezone file on target system.'.format(self.tz))
|
|
raise ValueError('Invalid timezone')
|
|
return(tzfilebase)
|
|
|
|
def apply(self):
|
|
tzsrcfile = os.path.join('/', self._verify())
|
|
tzdestfile = os.path.join(self.chroot_base, 'etc', 'localtime')
|
|
if os.path.isfile(tzdestfile):
|
|
os.remove(tzdestfile)
|
|
os.symlink(tzsrcfile, tzdestfile)
|
|
_logger.info('Created symlink: {0} => {1}'.format(tzsrcfile, tzdestfile))
|
|
return(None)
|