From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from lists.gentoo.org (pigeon.gentoo.org [208.92.234.80]) by finch.gentoo.org (Postfix) with ESMTP id 1A8601381F3 for ; Wed, 11 Sep 2013 11:14:57 +0000 (UTC) Received: from pigeon.gentoo.org (localhost [127.0.0.1]) by pigeon.gentoo.org (Postfix) with SMTP id 8D870E0CA7; Wed, 11 Sep 2013 11:14:56 +0000 (UTC) Received: from smtp.gentoo.org (smtp.gentoo.org [140.211.166.183]) (using TLSv1 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by pigeon.gentoo.org (Postfix) with ESMTPS id E5F7CE0CA7 for ; Wed, 11 Sep 2013 11:14:55 +0000 (UTC) Received: from hornbill.gentoo.org (hornbill.gentoo.org [94.100.119.163]) (using TLSv1 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by smtp.gentoo.org (Postfix) with ESMTPS id C704133EB81 for ; Wed, 11 Sep 2013 11:14:54 +0000 (UTC) Received: from localhost.localdomain (localhost [127.0.0.1]) by hornbill.gentoo.org (Postfix) with ESMTP id 652BAE5465 for ; Wed, 11 Sep 2013 11:14:52 +0000 (UTC) From: "André Erdmann" To: gentoo-commits@lists.gentoo.org Content-Transfer-Encoding: 8bit Content-type: text/plain; charset=UTF-8 Reply-To: gentoo-dev@lists.gentoo.org, "André Erdmann" Message-ID: <1378898012.03126a24a417833c13b4f27e3e7996d628aef988.dywi@gentoo> Subject: [gentoo-commits] proj/R_overlay:master commit in: roverlay/setupscript/, roverlay/ X-VCS-Repository: proj/R_overlay X-VCS-Files: roverlay/runtime.py roverlay/setupscript.py roverlay/setupscript/__init__.py roverlay/setupscript/baseenv.py roverlay/setupscript/hookenv.py roverlay/setupscript/initenv.py roverlay/setupscript/runtime.py X-VCS-Directories: roverlay/setupscript/ roverlay/ X-VCS-Committer: dywi X-VCS-Committer-Name: André Erdmann X-VCS-Revision: 03126a24a417833c13b4f27e3e7996d628aef988 X-VCS-Branch: master Date: Wed, 11 Sep 2013 11:14:52 +0000 (UTC) Precedence: bulk List-Post: List-Help: List-Unsubscribe: List-Subscribe: List-Id: Gentoo Linux mail X-BeenThere: gentoo-commits@lists.gentoo.org X-Archives-Salt: b0fc89b0-022f-48b8-ad31-9e627fc6aab2 X-Archives-Hash: b85d984455ad09730e6e8059969e5f3f commit: 03126a24a417833c13b4f27e3e7996d628aef988 Author: André Erdmann mailerd de> AuthorDate: Wed Sep 11 11:13:32 2013 +0000 Commit: André Erdmann mailerd de> CommitDate: Wed Sep 11 11:13:32 2013 +0000 URL: http://git.overlays.gentoo.org/gitweb/?p=proj/R_overlay.git;a=commit;h=03126a24 split roverlay/setupscript.py into several modules --- roverlay/runtime.py | 13 + roverlay/setupscript.py | 1069 -------------------------------------- roverlay/setupscript/__init__.py | 2 + roverlay/setupscript/baseenv.py | 72 +++ roverlay/setupscript/hookenv.py | 230 ++++++++ roverlay/setupscript/initenv.py | 308 +++++++++++ roverlay/setupscript/runtime.py | 474 +++++++++++++++++ 7 files changed, 1099 insertions(+), 1069 deletions(-) diff --git a/roverlay/runtime.py b/roverlay/runtime.py index 5ef540d..68580f0 100644 --- a/roverlay/runtime.py +++ b/roverlay/runtime.py @@ -272,6 +272,14 @@ class IndependentRuntimeEnvironment ( MinimalRuntimeEnvironment ): LOG_FORMAT = None LOG_LEVEL = None + @classmethod + def run_default_main ( cls, *args, **kwargs ): + instance = cls ( *args, **kwargs ) + instance.setup() + instance.default_main() + return instance + # --- end of run_default_main (...) --- + def __init__ ( self, installed=True, stdout=None, stderr=None ): super ( IndependentRuntimeEnvironment, self ).__init__() @@ -295,6 +303,11 @@ class IndependentRuntimeEnvironment ( MinimalRuntimeEnvironment ): self.INSTALLINFO = None # --- end of __init__ (...) --- + @roverlay.util.objects.abstractmethod + def default_main ( self ): + pass + # --- end of default_main (...) --- + def create_new_config ( self, config_str=None, apply_defaults=True ): ctree = roverlay.config.tree.ConfigTree ( register_static=False ) diff --git a/roverlay/setupscript.py b/roverlay/setupscript.py deleted file mode 100644 index 0e59015..0000000 --- a/roverlay/setupscript.py +++ /dev/null @@ -1,1069 +0,0 @@ -# R overlay -- setup script -# -*- coding: utf-8 -*- - -from __future__ import print_function - -import argparse -import collections -import errno -import logging -import os -import shutil -import stat -import sys -import textwrap - -import roverlay.argutil -import roverlay.argparser -import roverlay.fsutil -import roverlay.runtime - -import roverlay.config.defconfig -import roverlay.config.entrymap -import roverlay.config.entryutil - -import roverlay.static.hookinfo - -import roverlay.util.counter - -if sys.hexversion >= 0x3000000: - read_user_input = input -else: - read_user_input = raw_input - - - - - - -def arg_stdout_or_fs ( value ): - if value == '-': - return value - else: - return os.path.abspath ( os.path.expanduser ( value ) ) -# --- end of arg_stdout_or_fs (...) --- - - - - -class SetupArgParser ( roverlay.argparser.RoverlayArgumentParser ): - MULTIPLE_COMMANDS = False - COMMAND_DESCRIPTION = { - 'init': 'initialize roverlay\'s config and filesystem layout', - 'mkconfig': 'generate a config file', - } - DEFAULT_COMMAND = "init" - - COMMANDS_WITH_PRETEND = frozenset ({ 'init', }) - - SETUP_TARGETS = ( 'version', 'actions', 'setup', 'config', 'init', ) - PARSE_TARGETS = ( 'actions', 'setup', 'config', 'init', ) - - - def setup_setup ( self ): - arg = self.setup_setup_minimal ( title='common options' ) - - arg ( - '--output', '-O', metavar="", dest='output', - default='-', type=arg_stdout_or_fs, - flags=self.ARG_WITH_DEFAULT, - help='output file/dir/stream used by various commands (\'-\' for stdout)', - ) - - arg ( - '--pretend', '-p', dest='pretend', - flags=self.ARG_WITH_DEFAULT|self.ARG_OPT_IN, - help='show what would be done', - ) - - arg ( - '--ask', '-a', dest='wait_confirm', - flags=self.ARG_WITH_DEFAULT|self.ARG_OPT_IN, - help='get confirmation before actually doing anything', - ) - - return arg - # --- end of setup_setup (...) --- - - def parse_setup ( self ): - self.parse_setup_minimal() - - if self.parsed ['pretend']: - for cmd in self.get_commands(): - if cmd not in self.__class__.COMMANDS_WITH_PRETEND: - self.parser.exit ( - "{!r} command does not support --pretend.".format ( cmd ) - ) - # --- end of parse_setup (...) --- - - def setup_config ( self ): - arg = self.add_argument_group ( - "config", title="options for the main config file" - ) - - arg ( - '--expand-user', dest='config_expand_user', - flags=self.ARG_WITH_DEFAULT|self.ARG_OPT_IN, - help="expand \'~\' to the target user\'s home directory", - ) - - arg ( - '--additions-dir', '-A', dest='additions_dir', - flags=self.ARG_WITH_DEFAULT|self.ARG_META_DIR, - type=roverlay.argutil.couldbe_dirstr_existing, - help=( - 'directory for user-provided content ' - '(patches, hand-written ebuilds, hooks)' - ), - ) - - arg ( - '--variable', '-v', metavar="", dest='config_vars', - default=[], action='append', - type=roverlay.argutil.is_config_opt, - help="additional variables", - ) - - return arg - # --- end of setup_config (...) --- - - def parse_config ( self ): - for kv in self.parsed ['config_vars']: - key, eq_sign, val = kv.partition('=') - if key in { 'ADDITIONS_DIR', 'OVERLAY_ADDITIONS_DIR' }: - self.parser.exit ( - 'use \'--additions-dir {0}\' instead of ' - '\'--variable ADDITIONS_DIR={0}\'.'.format ( val ) - ) - - self.parsed ['config_vars'].append ( - "ADDITIONS_DIR=" + self.parsed ['additions_dir'] - ) - # --- end of parse_config (...) --- - - def setup_init ( self ): - arg = self.add_argument_group ( - 'init', title='options for the \'init\' command' - ) - - arg ( - '--enable-default-hooks', dest='want_default_hooks', - default=True, - flags=self.ARG_WITH_DEFAULT|self.ARG_OPT_IN, - help='enable/update the default hooks', - ) - - arg ( - '--no-default-hooks', dest='want_default_hooks', - flags=self.ARG_SHARED|self.ARG_OPT_OUT, - help='disable the default hooks', - ) - - arg ( - '--import-config', '-I', dest='import_config', - default="symlink", - choices=[ - "disable", - "symlink", "symlink=root", - "symlink=dirs", "symlink=files", - "copy" - ], - metavar='', - flags=self.ARG_WITH_DEFAULT, - help=( - 'choose whether and how --conf-root should be imported: ' - '%(choices)s' - ), - ) - - arg ( - '--no-import-config', dest='import_config', - action='store_const', const='disable', - help='disable config import (same as \'--import-config disable\')', - ) - - - arg ( - '--target-uid', dest='target_uid', default=os.getuid(), - metavar='', type=roverlay.argutil.is_uid, - flags=self.ARG_WITH_DEFAULT, - help="uid of the user that will run roverlay", - ) - - arg ( - '--target-gid', dest='target_gid', default=os.getgid(), - metavar='', type=roverlay.argutil.is_gid, - flags=self.ARG_WITH_DEFAULT, - help='gid of the user that will run roverlay', - ) - - # --- end of setup_init (...) --- - - def parse_init ( self ): - my_uid = os.getuid() - - if my_uid and self.parsed ['target_uid'] != my_uid: - if self.parsed ['pretend']: - sys.stderr.write ( - "!!! --target-uid: users cannot configure other users.\n\n" - ) - else: - self.parser.exit ( - "--target-uid: users cannot configure other users." - ) - # --- end of parse_init (...) --- - - -# --- end of SetupArgParser --- - - -class SetupEnvironment ( roverlay.runtime.IndependentRuntimeEnvironment ): - - LOG_LEVEL = logging.INFO - - SHARED_DIR_MODE = roverlay.fsutil.get_stat_mode ( "rwxrwxr-x" ) - PRIVATE_DIR_MODE = roverlay.fsutil.get_stat_mode ( "rwxr-x---" ) - SHARED_FILE_MODE = roverlay.fsutil.get_stat_mode ( "rw-rw-r--" ) - PRIVATE_FILE_MODE = roverlay.fsutil.get_stat_mode ( "rw-r-----" ) - - def __init__ ( self, *args, **kwargs ): - super ( SetupEnvironment, self ).__init__ ( *args, **kwargs ) - - self.UID = os.getuid() - self.GID = os.getgid() - - self.expanduser = None - self.fs_ops = None - self.fs_ops_virtual = None - - self.want_chown = None - self.data_root = None - self.work_root = None - self.conf_root = None - self.user_conf_root = None - -# not used -# COLUMNS = os.environ.get ( 'COLUMNS', 78 ) -# -# self.text_wrapper = textwrap.TextWrapper ( -# width=COLUMNS, initial_indent='', subsequent_indent='', -# break_long_words=False, break_on_hyphens=False, -# ) - # --- end of __init__ (...) --- - - def create_argparser ( self ): - instinfo = self.access_constant ( 'INSTALLINFO' ) - - return SetupArgParser ( - description = 'roverlay setup script', - defaults = { - 'work_root' : instinfo ['workroot'], - 'data_root' : instinfo ['libexec'], - 'conf_root' : instinfo ['confroot'], - 'private_conf_root' : instinfo ['workroot'] + os.sep + 'config', - 'import_config' : 'symlink=root', - 'additions_dir' : instinfo ['workroot'] + os.sep + 'files', - }, - ) - # --- end of create_argparser (...) --- - - def _get_config_roots ( self ): - return ( - os.path.realpath ( self.conf_root ), - os.path.realpath ( self.user_conf_root ) - ) - # --- end of get_config_roots (...) --- - - def get_user_config_root ( self ): - conf_root, user_conf_root = self._get_config_roots() - if conf_root == user_conf_root: - return None - else: - return user_conf_root - # --- end of get_user_config_root (...) --- - - def get_config_file_path ( self ): - return ( - self.work_root + os.sep + self.access_constant ( 'config_file_name' ) - ) - # --- end of get_config_file_path (...) --- - - def create_new_target_config ( self ): - return self.create_new_config ( - config_str=self.create_config_file ( expand_user=True ) - ) - # --- end of create_new_target_config (...) --- - - def _expanduser_pwd ( self, fspath ): - return roverlay.fsutil.pwd_expanduser ( - fspath, self.options ['target_uid'] - ) - # --- end of _expanduser_pwd (...) --- - - def create_config_file ( self, expand_user=False ): - conf_creator = roverlay.config.defconfig.RoverlayConfigCreation ( - is_installed = self.is_installed(), - work_root = ( - self.work_root if expand_user else self.options ['work_root'] - ), - data_root = ( - self.data_root if expand_user else self.options ['data_root'] - ), - conf_root = ( - self.user_conf_root if expand_user - else self.options ['private_conf_root'] - ), - ) - - for kv in self.options ['config_vars']: - key, sepa, value = kv.partition ( '=' ) - if not sepa: - raise Exception ( "bad variable given: {!r}".format ( kv ) ) - else: - conf_creator.set_option ( key, value ) - - return conf_creator.get_str() - # --- end of create_config_file (...) --- - - def write_config_file ( self, output=None, expand_user=None ): - config_file_str = self.create_config_file ( - expand_user = ( - self.options ['config_expand_user'] if expand_user is None - else expand_user - ), - ) - if not output or output == '-': - self.info ( config_file_str ) - else: - with open ( output, 'wt' ) as FH: - FH.write ( config_file_str ) - # --- end of write_config_file (...) --- - - def auto_reconfigure ( self ): - self.reconfigure ( self.create_config_file() ) - # --- end of auto_reconfigure (...) --- - - def setup ( self ): - self.PWD_INITIAL = os.getcwd() - self.setup_common() - - # ref - options = self.options - - target_uid = options ['target_uid'] - target_gid = options ['target_gid'] - - self.want_chown = ( target_uid != self.UID or target_gid != self.GID ) - - if self.UID == target_uid: - expanduser = os.path.expanduser - self.expanduser = os.path.expanduser - else: - expanduser = self._expanduser_pwd - self.expanduser = self._expanduser_pwd - - self.work_root = expanduser ( options ['work_root'] ) - self.data_root = expanduser ( options ['data_root'] ) - self.conf_root = expanduser ( options ['conf_root'] ) - self.user_conf_root = expanduser ( options ['private_conf_root'] ) - - - self.fs_ops_virtual = { - 'private_dir': roverlay.fsutil.VirtualFsOperations ( - uid=target_uid, gid=target_gid, mode=self.PRIVATE_DIR_MODE, - ), - 'shared_dir': roverlay.fsutil.VirtualFsOperations ( - uid=target_uid, gid=target_gid, mode=self.SHARED_DIR_MODE, - ), - 'private_file': roverlay.fsutil.VirtualFsOperations ( - uid=target_uid, gid=target_gid, mode=self.PRIVATE_FILE_MODE, - ), - 'shared_file': roverlay.fsutil.VirtualFsOperations ( - uid=target_uid, gid=target_gid, mode=self.SHARED_FILE_MODE, - ), - } - - if options ['pretend']: - self.fs_ops = self.fs_ops_virtual - else: - self.fs_ops = { - 'private_dir': roverlay.fsutil.FsOperations ( - uid=target_uid, gid=target_gid, mode=self.PRIVATE_DIR_MODE, - ), - 'shared_dir': roverlay.fsutil.FsOperations ( - uid=target_uid, gid=target_gid, mode=self.SHARED_DIR_MODE, - ), - 'private_file': roverlay.fsutil.FsOperations ( - uid=target_uid, gid=target_gid, mode=self.PRIVATE_FILE_MODE, - ), - 'shared_file': roverlay.fsutil.FsOperations ( - uid=target_uid, gid=target_gid, mode=self.SHARED_FILE_MODE, - ), - } - - # bind fs_ops - self.private_dir = self.fs_ops ['private_dir'] - self.shared_dir = self.fs_ops ['shared_dir'] - self.private_file = self.fs_ops ['private_file'] - self.shared_file = self.fs_ops ['shared_file'] - # --- end of setup (...) --- - - def wait_confirm ( self, - message=None, message_inline=None, - prepend_newline=True, append_newline=True - ): - try: - if prepend_newline: - self.info ( '\n' ) - - if message is not None: - self.info ( str ( message ) + '\n' ) - - if message_inline: - self.info ( - "Press Enter to continue ({!s}) ... ".format ( message_inline ) - ) - else: - self.info ( "Press Enter to continue ... " ) - - self.stdout.flush() - - ret = read_user_input().strip() - - except ( KeyboardInterrupt, EOFError ): - self.info ( "\n" ) - sys.exit ( 130 ) - else: - if append_newline: - self.info ( '\n' ) - - return ret - # --- end of wait_confirm (...) --- - - def wait_confirm_can_skip ( self, - SKIP_WORDS=frozenset({ 'skip', 'no', 'n'}), **kwargs - ): - assert 'message_inline' not in kwargs - - if self.options ['wait_confirm']: - - user_reply = self.wait_confirm ( - message_inline="type {} to skip this step".format ( - '/'.join ( repr( word ) for word in sorted( SKIP_WORDS ) ) - ), - **kwargs - ) - - return user_reply.lower() not in SKIP_WORDS - else: - return True - # --- end of wait_confirm (...) --- - - def get_init_env ( self ): - return SetupInitEnvironment ( self ) - # --- end of get_init_env (...) --- - - def get_hook_env ( self ): - return SetupHookEnvironment ( self ) - # --- end of get_hook_env (...) --- - -# --- end of SetupEnvironment --- - - -class SetupSubEnvironment ( object ): - - NEEDS_CONFIG_TREE = False - - ACTIONS = None - - def __init__ ( self, setup_env ): - super ( SetupSubEnvironment, self ).__init__() - - self.setup_env = setup_env - self.stdout = setup_env.stdout - self.stderr = setup_env.stderr - self.info = setup_env.info - self.error = setup_env.error - - if self.NEEDS_CONFIG_TREE: - self.config = self.setup_env.create_new_target_config() - else: - self.config = None - - self.setup() - # --- end of __init__ (...) --- - - def setup ( self ): - pass - # --- end of setup (...) --- - - def run ( self, steps_to_skip=None, verbose_skip=True, steps=None ): - pretend = self.setup_env.options ['pretend'] - ACTIONS = steps if steps is not None else self.ACTIONS - - if ACTIONS: - if steps_to_skip: - methods_to_call = [ - ( - None if item[0] in steps_to_skip - else getattr ( self, 'do_' + item[0] ) - ) for item in ACTIONS - ] - else: - methods_to_call = [ - getattr ( self, 'do_' + item[0] ) for item in ACTIONS - ] - - wait_confirm_can_skip = self.setup_env.wait_confirm_can_skip - - - for method, action in zip ( methods_to_call, ACTIONS ): - if method is None: - if verbose_skip: - self.info ( "{}: skipped.\n".format ( action[0] ) ) - - elif not action[1]: - method ( pretend=pretend ) - - elif wait_confirm_can_skip ( - message=method.__doc__, append_newline=False - ): - method ( pretend=pretend ) - else: - self.info ( "skipped.\n" ) - - else: - raise NotImplementedError ( - "{}.{}()".format ( self.__class__.__name__, "do_all" ) - ) - # --- end of run (...) --- - - -# --- end of SetupSubEnvironment --- - - -class SetupInitEnvironment ( SetupSubEnvironment ): - - ACTIONS = ( - ( 'pretend', False ), - ( 'prepare_config_file', False ), - ( 'import_config', True ), - ( 'setupdirs', True ), - ( 'write_config_file', True ), - ( 'enable_default_hooks', True ), - ) - - NEEDS_CONFIG_TREE = True - - def setup ( self ): - self.config_file_str = None - # --- end of setup (...) --- - - IMPORT_CONFIG_DESC = { - 'disable' : False, - 'symlink' : 'symlink {conf_root} to {user_conf_root}', - 'symlink=root' : 'symlink {conf_root} to {user_conf_root}', - 'symlink=dirs' : ( - 'symlink files/dirs from {conf_root} to {user_conf_root}/' - ), - 'symlink=files' : ( - 'recursively copy {conf_root} to {user_conf_root}, ' - 'but symlink files instead of copying them' - ), - 'copy' : 'recursively copy {conf_root} to {user_conf_root}', - } - - def gen_pretend_options ( self ): - options = self.setup_env.options - - def get_option ( key ): - val = options [key] - if isinstance ( val, str ): - return val - elif hasattr ( val, '__iter__' ): - return ' '.join ( str(x) for x in val ) - elif isinstance ( val, bool ): - return "yes" if val else "no" - elif val is None: - return "" - else: - return str ( val ) - # --- end of get_option (...) --- - - comment_path = lambda a, b: a if a == b else ( a + ' (' + b + ')' ) - get_path_option = lambda k, b: comment_path ( get_option ( k ), b ) - - fmt_vars = { - 'conf_root' : options ['conf_root'], - 'user_conf_root': options ['private_conf_root'] - } - - yield ( "user/uid", get_option ( 'target_uid' ) ) - yield ( "group/gid", get_option ( 'target_gid' ) ) - - yield ( "work root", - get_path_option ( 'work_root', self.setup_env.work_root ) - ) - yield ( "data root", - get_path_option ( 'data_root', self.setup_env.data_root ) - ) - yield ( "roverlay\'s config root", - get_path_option ( 'conf_root', self.setup_env.conf_root ) - ) - yield ( "user\'s config root", - get_path_option ( 'private_conf_root', self.setup_env.user_conf_root ) - ) - - import_config = get_option ( 'import_config' ) - if import_config == 'disable': - yield ( "import config", "no" ) - else: - yield ( "import config", - "yes, " - + self.IMPORT_CONFIG_DESC [import_config].format ( **fmt_vars ) - ) - - yield ( "enable default hooks", get_option ( 'want_default_hooks' ) ) - yield ( "additional config variables", get_option ( 'config_vars' ) ) - # --- end of gen_pretend_options (...) --- - - def gen_pretend_lines ( self, append_newline=True ): - options = list ( self.gen_pretend_options() ) - COLUMNS = os.environ.get ( 'COLUMNS', 78 ) - desc_len = min ( COLUMNS // 2, - 1 + max ( len(desc) for desc, value in options ) - ) - - line_wrapper = textwrap.TextWrapper ( - width=COLUMNS, initial_indent='', - subsequent_indent=( (desc_len+5) * ' ' ), - break_long_words=False, break_on_hyphens=False, - ) - - - yield 'Configuration:' - for desc, value in options: - yield line_wrapper.fill ( - "- {desc:<{l}}: {value}".format ( - desc=desc, l=desc_len, value=value, - ) - ) - - if append_newline: - yield "" - # --- end of gen_pretend_lines (...) --- - - def do_pretend ( self, pretend ): - """Shows what would be done.""" - self.info ( '\n'.join ( self.gen_pretend_lines() ) + '\n' ) - # --- end of do_pretend (...) --- - - def do_prepare_config_file ( self, pretend ): - """Creates the config file (in memory).""" - self.config_file_str = self.setup_env.create_config_file ( - expand_user=self.setup_env.options ['config_expand_user'] - ) - # --- end of do_prepare_config_file (...) --- - - def do_import_config ( self, pretend ): - """Imports the config.""" - mode = self.setup_env.options ['import_config'] - fs_ops = self.setup_env.private_dir - user_conf_root = self.setup_env.get_user_config_root() - # assert os.path.isdir ( os.path.dirname(user_conf_root) == work_root ) - - if user_conf_root is None and ( - fs_ops.unlink ( self.setup_env.user_conf_root ) - ): - # config_root was a symlink - - if pretend: - user_conf_root = self.setup_env.user_conf_root - else: - user_conf_root = self.setup_env.get_user_config_root() - - # -- end if - - - if user_conf_root is None: - self.info ( - "user has no private config directory - skipping config import.\n" - ) - - elif mode in { 'symlink=root', 'symlink' }: - if not fs_ops.wipe ( user_conf_root ): - self.setup_env.die ( - "failed to remove {!r}.\n".format ( user_conf_root ) - ) - elif not fs_ops.symlink ( self.setup_env.conf_root, user_conf_root ): - self.setup_env.die ( - "could not create symlink to {!r}.".format ( - self.setup_env.conf_root - ) - ) - - - pass - else: - raise NotImplementedError ( mode ) - # --- end of do_import_config (...) --- - - def do_setupdirs ( self, pretend ): - """Creates directories with proper permissions.""" - create_subdir_check = roverlay.fsutil.create_subdir_check - config = self.config - find_config_path = roverlay.config.entryutil.find_config_path - dodir_private = self.setup_env.private_dir.dodir - dodir_shared = self.setup_env.shared_dir.dodir - - - WANT_USERDIR = roverlay.config.entrymap.WANT_USERDIR - WANT_PRIVATE = roverlay.config.entrymap.WANT_PRIVATE - WANT_FILEDIR = roverlay.config.entrymap.WANT_FILEDIR - - listlike = lambda a: ( - hasattr(a, '__iter__') and not isinstance(a, str) - ) - iter_values = lambda b: ( - () if b is None else (b if listlike(b) else ( b, )) - ) - - dirs_exclude = [ - create_subdir_check ( self.setup_env.conf_root ), - create_subdir_check ( self.setup_env.data_root ), - ] - if self.setup_env.get_user_config_root() is None: - dirs_exclude.append ( - create_subdir_check ( self.setup_env.user_conf_root ) - ) - else: - print ( self.setup_env.get_user_config_root() ) - - # don't print exclude/skip messages more than once per dir - dirs_already_excluded = set() - - private_dirs = set() - private_dirs_chown = set() - shared_dirs = set() - shared_dirs_chown = set() - - # it's not necessary to create all of the listed dirs because some of - # them are automatically created at runtime, but doing so results in - # a (mostly) complete filesystem layout - # - for config_key, entry in ( - roverlay.config.entrymap.CONFIG_ENTRY_MAP.items() - ): - if isinstance ( entry, dict ) and 'want_dir_create' in entry: - for value in iter_values ( - config.get ( find_config_path ( config_key ), None ) - ): - dirmask = entry ['want_dir_create'] - dirpath = ( - os.path.dirname ( value.rstrip ( os.sep ) ) - if dirmask & WANT_FILEDIR else value.rstrip ( os.sep ) - ) - - if not dirpath or dirpath in dirs_already_excluded: - pass - - elif any ( ex ( dirpath ) for ex in dirs_exclude ): - self.info ( - "setupdirs: excluding {!r}\n".format ( dirpath ) - ) - dirs_already_excluded.add ( dirpath ) - - elif os.path.islink ( dirpath ): - self.info ( - '{!r} is a symlink - skipping setupdir ' - 'actions.\n'.format ( dirpath ) - ) - dirs_already_excluded.add ( dirpath ) - - elif dirmask & WANT_USERDIR: - if dirmask & WANT_PRIVATE: - private_dirs_chown.add ( dirpath ) - else: - shared_dirs_chown.add ( dirpath ) - - elif dirmask & WANT_PRIVATE: - private_dirs.add ( dirpath ) - - else: - shared_dirs.add ( dirpath ) - # -- end for - - - private_dirs -= private_dirs_chown - shared_dirs_chown -= private_dirs - shared_dirs -= shared_dirs_chown - - for dirpath in shared_dirs: - dodir_shared ( dirpath, chown=False ) - - for dirpath in shared_dirs_chown: - dodir_shared ( dirpath, chown=True ) - - for dirpath in private_dirs: - dodir_private ( dirpath, chown=False ) - - for dirpath in private_dirs_chown: - dodir_private ( dirpath, chown=True ) - - self.setup_env.private_dir.chmod_chown ( self.setup_env.work_root ) - # --- end of do_setupdirs (...) --- - - def do_write_config_file ( self, pretend ): - """Writes the config file to disk.""" - cfile = self.setup_env.get_config_file_path() - if not self.config_file_str: - self.setup_env.die ( "no config file created!" ) - elif pretend: - self.info ( "Would write config file to {!r}.\n".format ( cfile ) ) - else: - with open ( cfile, 'wt' ) as FH: - FH.write ( self.config_file_str ) - - self.setup_env.private_file.chmod_chown ( cfile ) - # --- end of do_write_config_file (...) --- - - def do_enable_default_hooks ( self, pretend ): - """Enables the default hooks, e.g. git history creation.""" - hook_env = self.setup_env.get_hook_env() - if not hook_env.enable_defaults(): - self.setup_env.die ( "failed to enable hooks." ) - # --- end of do_enable_default_hooks (...) --- - -# --- end of SetupInitEnvironment --- - - -class HookScript ( object ): - - def __init__ ( self, fspath, filename=None ): - super ( HookScript, self ).__init__() - fname = ( - filename if filename is not None else os.path.basename ( fspath ) - ) - - self.fspath = fspath - self.name = os.path.splitext ( fname )[0] or fname - static_entry = roverlay.static.hookinfo.get ( self.name, None ) - - if static_entry is not None: - self.default_events = static_entry[0] - self.priority = static_entry[1] - self.is_hidden = static_entry[2] - else: - self.default_events = False - self.priority = None - self.is_hidden = False - # --- end of __init__ (...) --- - - def is_visible ( self ): - return not self.is_hidden and ( - self.priority is None or self.priority >= 0 - ) - # --- end of is_visible (...) --- - - def __str__ ( self ): - yesno = lambda k: 'y' if k else 'n' - return "<{cls} {name!r}, hidden={h} prio={p}>".format ( - cls=self.__class__.__name__, - name=self.name, - h=yesno ( self.is_hidden ), - p=( - "auto" if self.priority is None else - ( "IGNORE" if self.priority < 0 else self.priority ) - ), - ) - # --- end of __str__ (...) --- - - def set_priority_from_generator ( self, number_gen, only_if_unset=True ): - if self.priority is None: - self.priority = next ( number_gen ) - return True - elif only_if_unset or self.priority < 0: - return False - else: - self.priority = next ( number_gen ) - return True - # --- end of set_priority_from_generator (...) --- - - def get_dest_name ( self, file_ext='.sh', digit_len=2 ): - # file_ext has to be .sh, else the script doesn't get recognized - # by mux.sh - - prio = self.priority - if prio is None or prio < 0: - raise AssertionError ( "hook script has no priority." ) - - return "{prio:0>{l}d}-{fname}{f_ext}".format ( - prio=prio, fname=self.name, f_ext=file_ext, l=digit_len, - ) - # --- end of get_dest_name (...) --- - - -# --- end of HookScript --- - - -class HookScriptDir ( object ): - - def __init__ ( self, root ): - super ( HookScriptDir, self ).__init__() - - self.root = root - self._scripts = collections.OrderedDict() - # --- end of __init__ (...) --- - - def __bool__ ( self ): - return bool ( self._scripts ) - # --- end of __bool__ (...) --- - - def get_script ( self, name ): - script = self._scripts [name] - return script if script.is_visible() else None - # --- end of get_scripts (...) --- - - def iter_default_scripts ( self, unpack=False ): - if unpack: - for script in self._scripts.values(): - if script.default_events: - for event in script.default_events: - yield ( event, script ) - else: - for script in self._scripts.values(): - if script.default_events: - yield script - # --- end of iter_default_scripts (...) --- - - def get_default_scripts ( self ): - scripts = dict() - for event, script in self.iter_default_scripts ( unpack=True ): - if event not in scripts: - scripts [event] = [ script ] - else: - scripts [event].append ( script ) - - return scripts - # --- end of get_default_scripts (...) --- - - def iter_scripts ( self ): - for script in self._scripts.values(): - if script.is_visible(): - yield script - # --- end of iter_scripts (...) --- - - def scan ( self ): - root = self.root - try: - filenames = sorted ( os.listdir ( root ) ) - except OSError as oserr: - if oserr.errno != errno.ENOENT: - raise - - else: - for fname in filenames: - fspath = root + os.sep + fname - if os.path.isfile ( fspath ): - script_obj = HookScript ( fspath, fname ) - self._scripts [script_obj.name] = script_obj - # --- end of scan (...) --- - -# --- end of HookScriptDir --- - - -class SetupHookEnvironment ( SetupSubEnvironment ): - - NEEDS_CONFIG_TREE = True - - def setup ( self ): - additions_dir = self.config.get ( 'OVERLAY.additions_dir', None ) - if additions_dir: - self.user_hook_root = os.path.join ( additions_dir, 'hooks' ) - self.writable = self.setup_env.private_file.check_writable ( - self.user_hook_root + os.sep + '.keep' - ) - else: - self.user_hook_root = None - self.writable = None - - self.hook_root = HookScriptDir ( - os.path.join ( self.setup_env.data_root, 'hooks' ) - ) - self.hook_root.scan() - self._prio_gen = roverlay.util.counter.UnsafeCounter ( 30 ) - # --- end of setup (...) --- - - def _link_hook ( self, source, link ): - if os.path.lexists ( link ): - linkdest = os.path.realpath ( link ) - - message = 'Skipping activation of hook {!r} - '.format ( link ) - - if linkdest == source or linkdest == os.path.realpath ( source ): - self.info ( message + "already set up.\n" ) - return True - - elif link != linkdest: - # symlink or link was relative - self.error ( message + "is a link to another file.\n" ) - else: - self.error ( message + "exists, but is not a link.\n" ) - - return None - else: - return self.setup_env.private_file.symlink ( source, link ) - # --- end of _link_hook (...) --- - - def link_hooks_v ( self, event_name, hooks ): - success = False - - if self.writable and self.user_hook_root: - destdir = self.user_hook_root + os.sep + event_name - self.setup_env.private_dir.dodir ( destdir ) - - to_link = [] - for script in hooks: - script.set_priority_from_generator ( self._prio_gen ) - to_link.append ( - ( script.fspath, destdir + os.sep + script.get_dest_name() ) - ) - - success = True - for source, link_name in to_link: - if self._link_hook ( source, link_name ) is False: - success = False - # -- end if - - return success - # --- end of link_hooks_v (...) --- - - def enable_defaults ( self ): - # not strict: missing hooks are ignored - success = False - if self.hook_root: - success = True - default_hooks = self.hook_root.get_default_scripts() - for event, hooks in default_hooks.items(): - if not self.link_hooks_v ( event, hooks ): - success = False - # -- end if - - return success - # --- end of enable_defaults (...) --- - - -# --- end of SetupHookEnvironment --- - - -def setup_main(): - env = SetupEnvironment() - env.setup() - - if env.wants_command ( "mkconfig" ): - env.write_config_file ( env.options ['output'] ) - elif env.wants_command ( "init" ): - env.get_init_env().run() - - -# --- end of setup_main (...) --- diff --git a/roverlay/setupscript/__init__.py b/roverlay/setupscript/__init__.py new file mode 100644 index 0000000..740a69c --- /dev/null +++ b/roverlay/setupscript/__init__.py @@ -0,0 +1,2 @@ +# R overlay -- setup script +# -*- coding: utf-8 -*- diff --git a/roverlay/setupscript/baseenv.py b/roverlay/setupscript/baseenv.py new file mode 100644 index 0000000..349d077 --- /dev/null +++ b/roverlay/setupscript/baseenv.py @@ -0,0 +1,72 @@ +# R overlay -- setup script, base env +# -*- coding: utf-8 -*- + +class SetupSubEnvironment ( object ): + + NEEDS_CONFIG_TREE = False + + ACTIONS = None + + def __init__ ( self, setup_env ): + super ( SetupSubEnvironment, self ).__init__() + + self.setup_env = setup_env + self.stdout = setup_env.stdout + self.stderr = setup_env.stderr + self.info = setup_env.info + self.error = setup_env.error + + if self.NEEDS_CONFIG_TREE: + self.config = self.setup_env.create_new_target_config() + else: + self.config = None + + self.setup() + # --- end of __init__ (...) --- + + def setup ( self ): + pass + # --- end of setup (...) --- + + def run ( self, steps_to_skip=None, verbose_skip=True, steps=None ): + pretend = self.setup_env.options ['pretend'] + ACTIONS = steps if steps is not None else self.ACTIONS + + if ACTIONS: + if steps_to_skip: + methods_to_call = [ + ( + None if item[0] in steps_to_skip + else getattr ( self, 'do_' + item[0] ) + ) for item in ACTIONS + ] + else: + methods_to_call = [ + getattr ( self, 'do_' + item[0] ) for item in ACTIONS + ] + + wait_confirm_can_skip = self.setup_env.wait_confirm_can_skip + + + for method, action in zip ( methods_to_call, ACTIONS ): + if method is None: + if verbose_skip: + self.info ( "{}: skipped.\n".format ( action[0] ) ) + + elif not action[1]: + method ( pretend=pretend ) + + elif wait_confirm_can_skip ( + message=method.__doc__, append_newline=False + ): + method ( pretend=pretend ) + else: + self.info ( "skipped.\n" ) + + else: + raise NotImplementedError ( + "{}.{}()".format ( self.__class__.__name__, "do_all" ) + ) + # --- end of run (...) --- + +# --- end of SetupSubEnvironment --- diff --git a/roverlay/setupscript/hookenv.py b/roverlay/setupscript/hookenv.py new file mode 100644 index 0000000..9c5fedb --- /dev/null +++ b/roverlay/setupscript/hookenv.py @@ -0,0 +1,230 @@ +# R overlay -- setup script, env for managing hooks +# -*- coding: utf-8 -*- + +import collections +import errno +import os + + +import roverlay.static.hookinfo +import roverlay.util.counter + + +class HookScript ( object ): + + def __init__ ( self, fspath, filename=None ): + super ( HookScript, self ).__init__() + fname = ( + filename if filename is not None else os.path.basename ( fspath ) + ) + + self.fspath = fspath + self.name = os.path.splitext ( fname )[0] or fname + static_entry = roverlay.static.hookinfo.get ( self.name, None ) + + if static_entry is not None: + self.default_events = static_entry[0] + self.priority = static_entry[1] + self.is_hidden = static_entry[2] + else: + self.default_events = False + self.priority = None + self.is_hidden = False + # --- end of __init__ (...) --- + + def is_visible ( self ): + return not self.is_hidden and ( + self.priority is None or self.priority >= 0 + ) + # --- end of is_visible (...) --- + + def __str__ ( self ): + yesno = lambda k: 'y' if k else 'n' + return "<{cls} {name!r}, hidden={h} prio={p}>".format ( + cls=self.__class__.__name__, + name=self.name, + h=yesno ( self.is_hidden ), + p=( + "auto" if self.priority is None else + ( "IGNORE" if self.priority < 0 else self.priority ) + ), + ) + # --- end of __str__ (...) --- + + def set_priority_from_generator ( self, number_gen, only_if_unset=True ): + if self.priority is None: + self.priority = next ( number_gen ) + return True + elif only_if_unset or self.priority < 0: + return False + else: + self.priority = next ( number_gen ) + return True + # --- end of set_priority_from_generator (...) --- + + def get_dest_name ( self, file_ext='.sh', digit_len=2 ): + # file_ext has to be .sh, else the script doesn't get recognized + # by mux.sh + + prio = self.priority + if prio is None or prio < 0: + raise AssertionError ( "hook script has no priority." ) + + return "{prio:0>{l}d}-{fname}{f_ext}".format ( + prio=prio, fname=self.name, f_ext=file_ext, l=digit_len, + ) + # --- end of get_dest_name (...) --- + + +# --- end of HookScript --- + + +class HookScriptDir ( object ): + + def __init__ ( self, root ): + super ( HookScriptDir, self ).__init__() + + self.root = root + self._scripts = collections.OrderedDict() + # --- end of __init__ (...) --- + + def __bool__ ( self ): + return bool ( self._scripts ) + # --- end of __bool__ (...) --- + + def get_script ( self, name ): + script = self._scripts [name] + return script if script.is_visible() else None + # --- end of get_scripts (...) --- + + def iter_default_scripts ( self, unpack=False ): + if unpack: + for script in self._scripts.values(): + if script.default_events: + for event in script.default_events: + yield ( event, script ) + else: + for script in self._scripts.values(): + if script.default_events: + yield script + # --- end of iter_default_scripts (...) --- + + def get_default_scripts ( self ): + scripts = dict() + for event, script in self.iter_default_scripts ( unpack=True ): + if event not in scripts: + scripts [event] = [ script ] + else: + scripts [event].append ( script ) + + return scripts + # --- end of get_default_scripts (...) --- + + def iter_scripts ( self ): + for script in self._scripts.values(): + if script.is_visible(): + yield script + # --- end of iter_scripts (...) --- + + def scan ( self ): + root = self.root + try: + filenames = sorted ( os.listdir ( root ) ) + except OSError as oserr: + if oserr.errno != errno.ENOENT: + raise + + else: + for fname in filenames: + fspath = root + os.sep + fname + if os.path.isfile ( fspath ): + script_obj = HookScript ( fspath, fname ) + self._scripts [script_obj.name] = script_obj + # --- end of scan (...) --- + +# --- end of HookScriptDir --- + + +class SetupHookEnvironment ( + roverlay.setupscript.baseenv.SetupSubEnvironment +): + + NEEDS_CONFIG_TREE = True + + def setup ( self ): + additions_dir = self.config.get ( 'OVERLAY.additions_dir', None ) + if additions_dir: + self.user_hook_root = os.path.join ( additions_dir, 'hooks' ) + self.writable = self.setup_env.private_file.check_writable ( + self.user_hook_root + os.sep + '.keep' + ) + else: + self.user_hook_root = None + self.writable = None + + self.hook_root = HookScriptDir ( + os.path.join ( self.setup_env.data_root, 'hooks' ) + ) + self.hook_root.scan() + self._prio_gen = roverlay.util.counter.UnsafeCounter ( 30 ) + # --- end of setup (...) --- + + def _link_hook ( self, source, link ): + if os.path.lexists ( link ): + linkdest = os.path.realpath ( link ) + + message = 'Skipping activation of hook {!r} - '.format ( link ) + + if linkdest == source or linkdest == os.path.realpath ( source ): + self.info ( message + "already set up.\n" ) + return True + + elif link != linkdest: + # symlink or link was relative + self.error ( message + "is a link to another file.\n" ) + else: + self.error ( message + "exists, but is not a link.\n" ) + + return None + else: + return self.setup_env.private_file.symlink ( source, link ) + # --- end of _link_hook (...) --- + + def link_hooks_v ( self, event_name, hooks ): + success = False + + if self.writable and self.user_hook_root: + destdir = self.user_hook_root + os.sep + event_name + self.setup_env.private_dir.dodir ( destdir ) + + to_link = [] + for script in hooks: + script.set_priority_from_generator ( self._prio_gen ) + to_link.append ( + ( script.fspath, destdir + os.sep + script.get_dest_name() ) + ) + + success = True + for source, link_name in to_link: + if self._link_hook ( source, link_name ) is False: + success = False + # -- end if + + return success + # --- end of link_hooks_v (...) --- + + def enable_defaults ( self ): + # not strict: missing hooks are ignored + success = False + if self.hook_root: + success = True + default_hooks = self.hook_root.get_default_scripts() + for event, hooks in default_hooks.items(): + if not self.link_hooks_v ( event, hooks ): + success = False + # -- end if + + return success + # --- end of enable_defaults (...) --- + +# --- end of SetupHookEnvironment --- diff --git a/roverlay/setupscript/initenv.py b/roverlay/setupscript/initenv.py new file mode 100644 index 0000000..f07953a --- /dev/null +++ b/roverlay/setupscript/initenv.py @@ -0,0 +1,308 @@ +# R overlay -- setup script, env for the "init" command +# -*- coding: utf-8 -*- + +import os +import textwrap + + +import roverlay.fsutil + +import roverlay.config.entrymap +import roverlay.config.entryutil + +import roverlay.setupscript.baseenv + + +class SetupInitEnvironment ( + roverlay.setupscript.baseenv.SetupSubEnvironment +): + + ACTIONS = ( + ( 'pretend', False ), + ( 'prepare_config_file', False ), + ( 'import_config', True ), + ( 'setupdirs', True ), + ( 'write_config_file', True ), + ( 'enable_default_hooks', True ), + ) + + NEEDS_CONFIG_TREE = True + + def setup ( self ): + self.config_file_str = None + # --- end of setup (...) --- + + IMPORT_CONFIG_DESC = { + 'disable' : False, + 'symlink' : 'symlink {conf_root} to {user_conf_root}', + 'symlink=root' : 'symlink {conf_root} to {user_conf_root}', + 'symlink=dirs' : ( + 'symlink files/dirs from {conf_root} to {user_conf_root}/' + ), + 'symlink=files' : ( + 'recursively copy {conf_root} to {user_conf_root}, ' + 'but symlink files instead of copying them' + ), + 'copy' : 'recursively copy {conf_root} to {user_conf_root}', + } + + def gen_pretend_options ( self ): + options = self.setup_env.options + + def get_option ( key ): + val = options [key] + if isinstance ( val, str ): + return val + elif hasattr ( val, '__iter__' ): + return ' '.join ( str(x) for x in val ) + elif isinstance ( val, bool ): + return "yes" if val else "no" + elif val is None: + return "" + else: + return str ( val ) + # --- end of get_option (...) --- + + comment_path = lambda a, b: a if a == b else ( a + ' (' + b + ')' ) + get_path_option = lambda k, b: comment_path ( get_option ( k ), b ) + + fmt_vars = { + 'conf_root' : options ['conf_root'], + 'user_conf_root': options ['private_conf_root'] + } + + yield ( "user/uid", get_option ( 'target_uid' ) ) + yield ( "group/gid", get_option ( 'target_gid' ) ) + + yield ( "work root", + get_path_option ( 'work_root', self.setup_env.work_root ) + ) + yield ( "data root", + get_path_option ( 'data_root', self.setup_env.data_root ) + ) + yield ( "roverlay\'s config root", + get_path_option ( 'conf_root', self.setup_env.conf_root ) + ) + yield ( "user\'s config root", + get_path_option ( 'private_conf_root', self.setup_env.user_conf_root ) + ) + + import_config = get_option ( 'import_config' ) + if import_config == 'disable': + yield ( "import config", "no" ) + else: + yield ( "import config", + "yes, " + + self.IMPORT_CONFIG_DESC [import_config].format ( **fmt_vars ) + ) + + yield ( "enable default hooks", get_option ( 'want_default_hooks' ) ) + yield ( "additional config variables", get_option ( 'config_vars' ) ) + # --- end of gen_pretend_options (...) --- + + def gen_pretend_lines ( self, append_newline=True ): + options = list ( self.gen_pretend_options() ) + COLUMNS = os.environ.get ( 'COLUMNS', 78 ) + desc_len = min ( COLUMNS // 2, + 1 + max ( len(desc) for desc, value in options ) + ) + + line_wrapper = textwrap.TextWrapper ( + width=COLUMNS, initial_indent='', + subsequent_indent=( (desc_len+5) * ' ' ), + break_long_words=False, break_on_hyphens=False, + ) + + + yield 'Configuration:' + for desc, value in options: + yield line_wrapper.fill ( + "- {desc:<{l}}: {value}".format ( + desc=desc, l=desc_len, value=value, + ) + ) + + if append_newline: + yield "" + # --- end of gen_pretend_lines (...) --- + + def do_pretend ( self, pretend ): + """Shows what would be done.""" + self.info ( '\n'.join ( self.gen_pretend_lines() ) + '\n' ) + # --- end of do_pretend (...) --- + + def do_prepare_config_file ( self, pretend ): + """Creates the config file (in memory).""" + self.config_file_str = self.setup_env.create_config_file ( + expand_user=self.setup_env.options ['config_expand_user'] + ) + # --- end of do_prepare_config_file (...) --- + + def do_import_config ( self, pretend ): + """Imports the config.""" + mode = self.setup_env.options ['import_config'] + fs_ops = self.setup_env.private_dir + user_conf_root = self.setup_env.get_user_config_root() + # assert os.path.isdir ( os.path.dirname(user_conf_root) == work_root ) + + if user_conf_root is None and ( + fs_ops.unlink ( self.setup_env.user_conf_root ) + ): + # config_root was a symlink + + if pretend: + user_conf_root = self.setup_env.user_conf_root + else: + user_conf_root = self.setup_env.get_user_config_root() + + # -- end if + + + if user_conf_root is None: + self.info ( + "user has no private config directory - skipping config import.\n" + ) + + elif mode in { 'symlink=root', 'symlink' }: + if not fs_ops.wipe ( user_conf_root ): + self.setup_env.die ( + "failed to remove {!r}.\n".format ( user_conf_root ) + ) + elif not fs_ops.symlink ( self.setup_env.conf_root, user_conf_root ): + self.setup_env.die ( + "could not create symlink to {!r}.".format ( + self.setup_env.conf_root + ) + ) + + + pass + else: + raise NotImplementedError ( mode ) + # --- end of do_import_config (...) --- + + def do_setupdirs ( self, pretend ): + """Creates directories with proper permissions.""" + create_subdir_check = roverlay.fsutil.create_subdir_check + config = self.config + find_config_path = roverlay.config.entryutil.find_config_path + dodir_private = self.setup_env.private_dir.dodir + dodir_shared = self.setup_env.shared_dir.dodir + + + WANT_USERDIR = roverlay.config.entrymap.WANT_USERDIR + WANT_PRIVATE = roverlay.config.entrymap.WANT_PRIVATE + WANT_FILEDIR = roverlay.config.entrymap.WANT_FILEDIR + + listlike = lambda a: ( + hasattr(a, '__iter__') and not isinstance(a, str) + ) + iter_values = lambda b: ( + () if b is None else (b if listlike(b) else ( b, )) + ) + + dirs_exclude = [ + create_subdir_check ( self.setup_env.conf_root ), + create_subdir_check ( self.setup_env.data_root ), + ] + if self.setup_env.get_user_config_root() is None: + dirs_exclude.append ( + create_subdir_check ( self.setup_env.user_conf_root ) + ) + + # don't print exclude/skip messages more than once per dir + dirs_already_excluded = set() + + private_dirs = set() + private_dirs_chown = set() + shared_dirs = set() + shared_dirs_chown = set() + + # it's not necessary to create all of the listed dirs because some of + # them are automatically created at runtime, but doing so results in + # a (mostly) complete filesystem layout + # + for config_key, entry in ( + roverlay.config.entrymap.CONFIG_ENTRY_MAP.items() + ): + if isinstance ( entry, dict ) and 'want_dir_create' in entry: + for value in iter_values ( + config.get ( find_config_path ( config_key ), None ) + ): + dirmask = entry ['want_dir_create'] + dirpath = ( + os.path.dirname ( value.rstrip ( os.sep ) ) + if dirmask & WANT_FILEDIR else value.rstrip ( os.sep ) + ) + + if not dirpath or dirpath in dirs_already_excluded: + pass + + elif any ( ex ( dirpath ) for ex in dirs_exclude ): + self.info ( + "setupdirs: excluding {!r}\n".format ( dirpath ) + ) + dirs_already_excluded.add ( dirpath ) + + elif os.path.islink ( dirpath ): + self.info ( + '{!r} is a symlink - skipping setupdir ' + 'actions.\n'.format ( dirpath ) + ) + dirs_already_excluded.add ( dirpath ) + + elif dirmask & WANT_USERDIR: + if dirmask & WANT_PRIVATE: + private_dirs_chown.add ( dirpath ) + else: + shared_dirs_chown.add ( dirpath ) + + elif dirmask & WANT_PRIVATE: + private_dirs.add ( dirpath ) + + else: + shared_dirs.add ( dirpath ) + # -- end for + + + private_dirs -= private_dirs_chown + shared_dirs_chown -= private_dirs + shared_dirs -= shared_dirs_chown + + for dirpath in shared_dirs: + dodir_shared ( dirpath, chown=False ) + + for dirpath in shared_dirs_chown: + dodir_shared ( dirpath, chown=True ) + + for dirpath in private_dirs: + dodir_private ( dirpath, chown=False ) + + for dirpath in private_dirs_chown: + dodir_private ( dirpath, chown=True ) + + self.setup_env.private_dir.chmod_chown ( self.setup_env.work_root ) + # --- end of do_setupdirs (...) --- + + def do_write_config_file ( self, pretend ): + """Writes the config file to disk.""" + cfile = self.setup_env.get_config_file_path() + if not self.config_file_str: + self.setup_env.die ( "no config file created!" ) + elif pretend: + self.info ( "Would write config file to {!r}.\n".format ( cfile ) ) + else: + with open ( cfile, 'wt' ) as FH: + FH.write ( self.config_file_str ) + + self.setup_env.private_file.chmod_chown ( cfile ) + # --- end of do_write_config_file (...) --- + + def do_enable_default_hooks ( self, pretend ): + """Enables the default hooks, e.g. git history creation.""" + hook_env = self.setup_env.get_hook_env() + if not hook_env.enable_defaults(): + self.setup_env.die ( "failed to enable hooks." ) + # --- end of do_enable_default_hooks (...) --- + +# --- end of SetupInitEnvironment --- diff --git a/roverlay/setupscript/runtime.py b/roverlay/setupscript/runtime.py new file mode 100644 index 0000000..9ba3eec --- /dev/null +++ b/roverlay/setupscript/runtime.py @@ -0,0 +1,474 @@ +# R overlay -- setup script, runtime +# -*- coding: utf-8 -*- + +from __future__ import print_function + +import logging +import os +import sys +#import textwrap + +import roverlay.argparser +import roverlay.argutil +import roverlay.fsutil +import roverlay.runtime + +import roverlay.config.defconfig + +import roverlay.setupscript.initenv +import roverlay.setupscript.hookenv + + + +if sys.hexversion >= 0x3000000: + read_user_input = input +else: + read_user_input = raw_input + + + +def setup_main_installed(): + return setup_main ( True ) +# --- end of setup_main_installed (...) --- + +def setup_main ( installed ): + SetupEnvironment.run_default_main ( installed=installed ) + return os.EX_OK +# --- end of setup_main (...) --- + +def arg_stdout_or_fs ( value ): + if value == '-': + return value + else: + return os.path.abspath ( os.path.expanduser ( value ) ) +# --- end of arg_stdout_or_fs (...) --- + + +class SetupArgParser ( roverlay.argparser.RoverlayArgumentParser ): + MULTIPLE_COMMANDS = False + COMMAND_DESCRIPTION = { + 'init': 'initialize roverlay\'s config and filesystem layout', + 'mkconfig': 'generate a config file', + } + DEFAULT_COMMAND = "init" + + COMMANDS_WITH_PRETEND = frozenset ({ 'init', }) + + SETUP_TARGETS = ( 'version', 'actions', 'setup', 'config', 'init', ) + PARSE_TARGETS = ( 'actions', 'setup', 'config', 'init', ) + + + def setup_setup ( self ): + arg = self.setup_setup_minimal ( title='common options' ) + + arg ( + '--output', '-O', metavar="", dest='output', + default='-', type=arg_stdout_or_fs, + flags=self.ARG_WITH_DEFAULT, + help='output file/dir/stream used by various commands (\'-\' for stdout)', + ) + + arg ( + '--pretend', '-p', dest='pretend', + flags=self.ARG_WITH_DEFAULT|self.ARG_OPT_IN, + help='show what would be done', + ) + + arg ( + '--ask', '-a', dest='wait_confirm', + flags=self.ARG_WITH_DEFAULT|self.ARG_OPT_IN, + help='get confirmation before actually doing anything', + ) + + return arg + # --- end of setup_setup (...) --- + + def parse_setup ( self ): + self.parse_setup_minimal() + + if self.parsed ['pretend']: + for cmd in self.get_commands(): + if cmd not in self.__class__.COMMANDS_WITH_PRETEND: + self.parser.exit ( + "{!r} command does not support --pretend.".format ( cmd ) + ) + # --- end of parse_setup (...) --- + + def setup_config ( self ): + arg = self.add_argument_group ( + "config", title="options for the main config file" + ) + + arg ( + '--expand-user', dest='config_expand_user', + flags=self.ARG_WITH_DEFAULT|self.ARG_OPT_IN, + help="expand \'~\' to the target user\'s home directory", + ) + + arg ( + '--additions-dir', '-A', dest='additions_dir', + flags=self.ARG_WITH_DEFAULT|self.ARG_META_DIR, + type=roverlay.argutil.couldbe_dirstr_existing, + help=( + 'directory for user-provided content ' + '(patches, hand-written ebuilds, hooks)' + ), + ) + + arg ( + '--variable', '-v', metavar="", dest='config_vars', + default=[], action='append', + type=roverlay.argutil.is_config_opt, + help="additional variables", + ) + + return arg + # --- end of setup_config (...) --- + + def parse_config ( self ): + for kv in self.parsed ['config_vars']: + key, eq_sign, val = kv.partition('=') + if key in { 'ADDITIONS_DIR', 'OVERLAY_ADDITIONS_DIR' }: + self.parser.exit ( + 'use \'--additions-dir {0}\' instead of ' + '\'--variable ADDITIONS_DIR={0}\'.'.format ( val ) + ) + + self.parsed ['config_vars'].append ( + "ADDITIONS_DIR=" + self.parsed ['additions_dir'] + ) + # --- end of parse_config (...) --- + + def setup_init ( self ): + arg = self.add_argument_group ( + 'init', title='options for the \'init\' command' + ) + + arg ( + '--enable-default-hooks', dest='want_default_hooks', + default=True, + flags=self.ARG_WITH_DEFAULT|self.ARG_OPT_IN, + help='enable/update the default hooks', + ) + + arg ( + '--no-default-hooks', dest='want_default_hooks', + flags=self.ARG_SHARED|self.ARG_OPT_OUT, + help='disable the default hooks', + ) + + arg ( + '--import-config', '-I', dest='import_config', + default="symlink", + choices=[ + "disable", + "symlink", "symlink=root", + "symlink=dirs", "symlink=files", + "copy" + ], + metavar='', + flags=self.ARG_WITH_DEFAULT, + help=( + 'choose whether and how --conf-root should be imported: ' + '%(choices)s' + ), + ) + + arg ( + '--no-import-config', dest='import_config', + action='store_const', const='disable', + help='disable config import (same as \'--import-config disable\')', + ) + + + arg ( + '--target-uid', dest='target_uid', default=os.getuid(), + metavar='', type=roverlay.argutil.is_uid, + flags=self.ARG_WITH_DEFAULT, + help="uid of the user that will run roverlay", + ) + + arg ( + '--target-gid', dest='target_gid', default=os.getgid(), + metavar='', type=roverlay.argutil.is_gid, + flags=self.ARG_WITH_DEFAULT, + help='gid of the user that will run roverlay', + ) + + # --- end of setup_init (...) --- + + def parse_init ( self ): + my_uid = os.getuid() + + if my_uid and self.parsed ['target_uid'] != my_uid: + if self.parsed ['pretend']: + sys.stderr.write ( + "!!! --target-uid: users cannot configure other users.\n\n" + ) + else: + self.parser.exit ( + "--target-uid: users cannot configure other users." + ) + # --- end of parse_init (...) --- + +# --- end of SetupArgParser --- + + +class SetupEnvironment ( roverlay.runtime.IndependentRuntimeEnvironment ): + + LOG_LEVEL = logging.INFO + + SHARED_DIR_MODE = roverlay.fsutil.get_stat_mode ( "rwxrwxr-x" ) + PRIVATE_DIR_MODE = roverlay.fsutil.get_stat_mode ( "rwxr-x---" ) + SHARED_FILE_MODE = roverlay.fsutil.get_stat_mode ( "rw-rw-r--" ) + PRIVATE_FILE_MODE = roverlay.fsutil.get_stat_mode ( "rw-r-----" ) + + def __init__ ( self, *args, **kwargs ): + super ( SetupEnvironment, self ).__init__ ( *args, **kwargs ) + + self.UID = os.getuid() + self.GID = os.getgid() + + self.expanduser = None + self.fs_ops = None + self.fs_ops_virtual = None + + self.want_chown = None + self.data_root = None + self.work_root = None + self.conf_root = None + self.user_conf_root = None + +# not used +# COLUMNS = os.environ.get ( 'COLUMNS', 78 ) +# +# self.text_wrapper = textwrap.TextWrapper ( +# width=COLUMNS, initial_indent='', subsequent_indent='', +# break_long_words=False, break_on_hyphens=False, +# ) + # --- end of __init__ (...) --- + + def create_argparser ( self ): + instinfo = self.access_constant ( 'INSTALLINFO' ) + + return SetupArgParser ( + description = 'roverlay setup script', + defaults = { + 'work_root' : instinfo ['workroot'], + 'data_root' : instinfo ['libexec'], + 'conf_root' : instinfo ['confroot'], + 'private_conf_root' : instinfo ['workroot'] + os.sep + 'config', + 'import_config' : 'symlink=root', + 'additions_dir' : instinfo ['workroot'] + os.sep + 'files', + }, + ) + # --- end of create_argparser (...) --- + + def _get_config_roots ( self ): + return ( + os.path.realpath ( self.conf_root ), + os.path.realpath ( self.user_conf_root ) + ) + # --- end of get_config_roots (...) --- + + def get_user_config_root ( self ): + conf_root, user_conf_root = self._get_config_roots() + if conf_root == user_conf_root: + return None + else: + return user_conf_root + # --- end of get_user_config_root (...) --- + + def get_config_file_path ( self ): + return ( + self.work_root + os.sep + self.access_constant ( 'config_file_name' ) + ) + # --- end of get_config_file_path (...) --- + + def create_new_target_config ( self ): + return self.create_new_config ( + config_str=self.create_config_file ( expand_user=True ) + ) + # --- end of create_new_target_config (...) --- + + def _expanduser_pwd ( self, fspath ): + return roverlay.fsutil.pwd_expanduser ( + fspath, self.options ['target_uid'] + ) + # --- end of _expanduser_pwd (...) --- + + def create_config_file ( self, expand_user=False ): + conf_creator = roverlay.config.defconfig.RoverlayConfigCreation ( + is_installed = self.is_installed(), + work_root = ( + self.work_root if expand_user else self.options ['work_root'] + ), + data_root = ( + self.data_root if expand_user else self.options ['data_root'] + ), + conf_root = ( + self.user_conf_root if expand_user + else self.options ['private_conf_root'] + ), + ) + + for kv in self.options ['config_vars']: + key, sepa, value = kv.partition ( '=' ) + if not sepa: + raise Exception ( "bad variable given: {!r}".format ( kv ) ) + else: + conf_creator.set_option ( key, value ) + + return conf_creator.get_str() + # --- end of create_config_file (...) --- + + def write_config_file ( self, output=None, expand_user=None ): + config_file_str = self.create_config_file ( + expand_user = ( + self.options ['config_expand_user'] if expand_user is None + else expand_user + ), + ) + if not output or output == '-': + self.info ( config_file_str ) + else: + with open ( output, 'wt' ) as FH: + FH.write ( config_file_str ) + # --- end of write_config_file (...) --- + + def auto_reconfigure ( self ): + self.reconfigure ( self.create_config_file() ) + # --- end of auto_reconfigure (...) --- + + def setup ( self ): + self.PWD_INITIAL = os.getcwd() + self.setup_common() + + # ref + options = self.options + + target_uid = options ['target_uid'] + target_gid = options ['target_gid'] + + self.want_chown = ( target_uid != self.UID or target_gid != self.GID ) + + if self.UID == target_uid: + expanduser = os.path.expanduser + self.expanduser = os.path.expanduser + else: + expanduser = self._expanduser_pwd + self.expanduser = self._expanduser_pwd + + self.work_root = expanduser ( options ['work_root'] ) + self.data_root = expanduser ( options ['data_root'] ) + self.conf_root = expanduser ( options ['conf_root'] ) + self.user_conf_root = expanduser ( options ['private_conf_root'] ) + + + self.fs_ops_virtual = { + 'private_dir': roverlay.fsutil.VirtualFsOperations ( + uid=target_uid, gid=target_gid, mode=self.PRIVATE_DIR_MODE, + ), + 'shared_dir': roverlay.fsutil.VirtualFsOperations ( + uid=target_uid, gid=target_gid, mode=self.SHARED_DIR_MODE, + ), + 'private_file': roverlay.fsutil.VirtualFsOperations ( + uid=target_uid, gid=target_gid, mode=self.PRIVATE_FILE_MODE, + ), + 'shared_file': roverlay.fsutil.VirtualFsOperations ( + uid=target_uid, gid=target_gid, mode=self.SHARED_FILE_MODE, + ), + } + + if options ['pretend']: + self.fs_ops = self.fs_ops_virtual + else: + self.fs_ops = { + 'private_dir': roverlay.fsutil.FsOperations ( + uid=target_uid, gid=target_gid, mode=self.PRIVATE_DIR_MODE, + ), + 'shared_dir': roverlay.fsutil.FsOperations ( + uid=target_uid, gid=target_gid, mode=self.SHARED_DIR_MODE, + ), + 'private_file': roverlay.fsutil.FsOperations ( + uid=target_uid, gid=target_gid, mode=self.PRIVATE_FILE_MODE, + ), + 'shared_file': roverlay.fsutil.FsOperations ( + uid=target_uid, gid=target_gid, mode=self.SHARED_FILE_MODE, + ), + } + + # bind fs_ops + self.private_dir = self.fs_ops ['private_dir'] + self.shared_dir = self.fs_ops ['shared_dir'] + self.private_file = self.fs_ops ['private_file'] + self.shared_file = self.fs_ops ['shared_file'] + # --- end of setup (...) --- + + def wait_confirm ( self, + message=None, message_inline=None, + prepend_newline=True, append_newline=True + ): + try: + if prepend_newline: + self.info ( '\n' ) + + if message is not None: + self.info ( str ( message ) + '\n' ) + + if message_inline: + self.info ( + "Press Enter to continue ({!s}) ... ".format ( message_inline ) + ) + else: + self.info ( "Press Enter to continue ... " ) + + self.stdout.flush() + + ret = read_user_input().strip() + + except ( KeyboardInterrupt, EOFError ): + self.info ( "\n" ) + sys.exit ( 130 ) + else: + if append_newline: + self.info ( '\n' ) + + return ret + # --- end of wait_confirm (...) --- + + def wait_confirm_can_skip ( self, + SKIP_WORDS=frozenset({ 'skip', 'no', 'n'}), **kwargs + ): + assert 'message_inline' not in kwargs + + if self.options ['wait_confirm']: + + user_reply = self.wait_confirm ( + message_inline="type {} to skip this step".format ( + '/'.join ( repr( word ) for word in sorted( SKIP_WORDS ) ) + ), + **kwargs + ) + + return user_reply.lower() not in SKIP_WORDS + else: + return True + # --- end of wait_confirm (...) --- + + def get_init_env ( self ): + return roverlay.setupscript.initenv.SetupInitEnvironment ( self ) + # --- end of get_init_env (...) --- + + def get_hook_env ( self ): + return roverlay.setupscript.hookenv.SetupHookEnvironment ( self ) + # --- end of get_hook_env (...) --- + + def default_main ( self ): + if self.wants_command ( "mkconfig" ): + self.write_config_file ( self.options ['output'] ) + elif self.wants_command ( "init" ): + self.get_init_env().run() + # --- end of default_main (...) --- + +# --- end of SetupEnvironment ---