xiwrap

slightly higher-level container setup utility
git clone https://git.ce9e.org/xiwrap.git

commit
ff86b4e2c3c0ec9a7e3d85a25b73e54ce0381996
parent
d297ffebebcdf94a90596928552cc489959130c1
Author
Tobias Bengfort <tobias.bengfort@posteo.de>
Date
2024-06-15 16:27
init

Diffstat

A rules/audio 2 ++
A rules/bin 4 ++++
A rules/graphics 3 +++
A rules/gtk 4 ++++
A rules/network 3 +++
A xiwrap.py 174 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

6 files changed, 190 insertions, 0 deletions


diff --git a/rules/audio b/rules/audio

@@ -0,0 +1,2 @@
   -1     1 env XDG_RUNTIME_DIR
   -1     2 ro-bind $XDG_RUNTIME_DIR/pulse

diff --git a/rules/bin b/rules/bin

@@ -0,0 +1,4 @@
   -1     1 ro-bind /bin
   -1     2 ro-bind /lib
   -1     3 ro-bind /lib64
   -1     4 ro-bind /usr

diff --git a/rules/graphics b/rules/graphics

@@ -0,0 +1,3 @@
   -1     1 env WAYLAND_DISPLAY
   -1     2 env XDG_RUNTIME_DIR
   -1     3 ro-bind $XDG_RUNTIME_DIR/$WAYLAND_DISPLAY

diff --git a/rules/gtk b/rules/gtk

@@ -0,0 +1,4 @@
   -1     1 import graphics
   -1     2 env HOME
   -1     3 ro-bind $HOME/.themes
   -1     4 ro-bind $HOME/.config/dconf

diff --git a/rules/network b/rules/network

@@ -0,0 +1,3 @@
   -1     1 share-net
   -1     2 ro-bind /etc/resolv.conf
   -1     3 ro-bind /etc/ssl/certs/ca-certificates.crt

diff --git a/xiwrap.py b/xiwrap.py

@@ -0,0 +1,174 @@
   -1     1 import os
   -1     2 import sys
   -1     3 from os.path import expandvars
   -1     4 from pathlib import Path
   -1     5 
   -1     6 USER_CONFIG = Path(os.getenv('XDG_CONFIG_HOME', '~/.config')) / 'xiwrap'
   -1     7 SYSTEM_CONFIG = Path('/etc') / 'xiwrap'
   -1     8 
   -1     9 USAGE = """Usage: xiwrap [OPTION]... -- [BWRAP_OPTIONS]... CMD
   -1    10 
   -1    11 Example: xiwrap --import bin --env TERM -- --chdir /tmp bash
   -1    12 
   -1    13 The following options are available:
   -1    14 
   -1    15 -h, --help              Print this message and exit
   -1    16 --debug                 Print the bwrap command instead of executing it.
   -1    17 --env VAR [VALUE]       Set an environment variable. If VALUE is not provided,
   -1    18                         the value from the current environment is kept.
   -1    19 --bind DEST [SRC]       Bind mount the host path SRC on DEST. If SRC is not
   -1    20                         provided, it is the same as DEST.
   -1    21 --ro-bind DEST [SRC]    Bind mount the host path SRC readonly on DEST. If SRC
   -1    22                         is not provided, it is the same as DEST.
   -1    23 --proc DEST             Mount new procfs on DEST.
   -1    24 --dev DEST              Mount new dev on DEST.
   -1    25 --tmpfs DEST            Mount new tmpfs on DEST.
   -1    26 --share-net             Do not create new network namespace.
   -1    27 --import FILE           Load additional options from FILE. FILE can be an
   -1    28                         absolute path or relative to the current directory,
   -1    29                         $XDG_CONFIG_HOME/xiwrap/ or /etc/xiwrap/. FILE must
   -1    30                         contain one option per line, without the leading --.
   -1    31                         Empty lines or lines starting with # are ignored.
   -1    32 """
   -1    33 
   -1    34 
   -1    35 class RuleError(ValueError):
   -1    36     def __init__(self, key, args):
   -1    37         rule = ' '.join([key, *args])
   -1    38         super().__init__(f'Invalid rule: {rule}')
   -1    39 
   -1    40 
   -1    41 class RuleSet:
   -1    42     def __init__(self):
   -1    43         self.env = {}
   -1    44         self.paths = {
   -1    45             '/tmp': ('tmpfs', None),
   -1    46             '/dev': ('dev', None),
   -1    47             '/proc': ('proc', None),
   -1    48         }
   -1    49         self.dbus = set()  # TODO
   -1    50         self.share_net = False
   -1    51         self.debug = False
   -1    52         self.usage = False
   -1    53 
   -1    54     def find_config_file(self, name, cwd):
   -1    55         if name.startswith('/'):
   -1    56             return Path(name)
   -1    57         elif name.startswith('~'):
   -1    58             return Path(name).expanduser()
   -1    59         for base in [cwd, USER_CONFIG, SYSTEM_CONFIG]:
   -1    60             path = base / name
   -1    61             if path.exists():
   -1    62                 return path
   -1    63         raise FileNotFoundError(name)
   -1    64 
   -1    65     def parse_env(self, key, args):
   -1    66         if len(args) == 2:
   -1    67             return args[0], args[1]
   -1    68         elif len(args) == 1:
   -1    69             return args[0], os.getenv(args[0])
   -1    70         else:
   -1    71             raise RuleError(key, args)
   -1    72 
   -1    73     def parse_path(self, key, args):
   -1    74         if len(args) == 2:
   -1    75             return args[0], args[1]
   -1    76         elif len(args) == 1:
   -1    77             return args[0], args[0]
   -1    78         else:
   -1    79             raise RuleError(key, args)
   -1    80 
   -1    81     def push_rule(self, key, args, *, cwd):
   -1    82         if key == 'import':
   -1    83             if len(args) != 1:
   -1    84                 raise RuleError(key, args)
   -1    85             path = self.find_config_file(args[0], cwd)
   -1    86             self.read_config_file(path)
   -1    87         elif key == 'share-net':
   -1    88             if len(args) != 0:
   -1    89                 raise RuleError(key, args)
   -1    90             self.share_net = True
   -1    91         elif key == 'env':
   -1    92             var, value = self.parse_env(key, args)
   -1    93             self.env[var] = value
   -1    94         elif key in ['ro-bind', 'bind']:
   -1    95             src, target = self.parse_path(key, args)
   -1    96             self.paths[expandvars(target)] = (key, expandvars(src))
   -1    97         elif key in ['tmpfs', 'dev', 'proc']:
   -1    98             if len(args) != 1:
   -1    99                 raise RuleError(key, args)
   -1   100             self.paths[expandvars(args[0])] = (key, None)
   -1   101         else:
   -1   102             raise RuleError(key, args)
   -1   103 
   -1   104     def read_config_file(self, path):
   -1   105         with open(path) as fh:
   -1   106             for lineno, line in enumerate(fh, start=1):
   -1   107                 line = line.strip()
   -1   108                 if not line or line.startswith('#'):
   -1   109                     continue
   -1   110                 try:
   -1   111                     parts = line.split()
   -1   112                     self.push_rule(parts[0], parts[1:], cwd=path.parent)
   -1   113                 except RuleError as e:
   -1   114                     raise SyntaxError(str(e), (path, lineno, 1, line)) from e
   -1   115 
   -1   116     def read_argv(self, argv):
   -1   117         key = None
   -1   118         args = []
   -1   119         for i, token in enumerate(argv):
   -1   120             if token == '--':
   -1   121                 if key is not None:
   -1   122                     self.push_rule(key, args, cwd=Path.cwd())
   -1   123                 return argv[i + 1:]
   -1   124             elif token in ['-h', '--help']:
   -1   125                 self.usage = True
   -1   126             elif token == '--debug':
   -1   127                 self.debug = True
   -1   128             elif token.startswith('--'):
   -1   129                 if key is not None:
   -1   130                     self.push_rule(key, args, cwd=Path.cwd())
   -1   131                 key = token.removeprefix('--')
   -1   132                 args = []
   -1   133             else:
   -1   134                 args.append(token)
   -1   135         raise ValueError('--')
   -1   136 
   -1   137     def build(self, bwrap_args):
   -1   138         cmd = [
   -1   139             'bwrap',
   -1   140             '--die-with-parent',
   -1   141             '--clearenv',
   -1   142             '--unshare-pid',
   -1   143         ]
   -1   144         if not self.share_net:
   -1   145             cmd += ['--unshare-net']
   -1   146         for key, value in self.env.items():
   -1   147             if value is not None:
   -1   148                 cmd += ['--setenv', key, value]
   -1   149         for target, value in sorted(self.paths.items()):
   -1   150             typ, src = value
   -1   151             if src is None:
   -1   152                 cmd += [f'--{typ}', target]
   -1   153             else:
   -1   154                 cmd += [f'--{typ}', src, target]
   -1   155         return cmd + bwrap_args
   -1   156 
   -1   157 
   -1   158 if __name__ == '__main__':
   -1   159     rules = RuleSet()
   -1   160     try:
   -1   161         tail = rules.read_argv(sys.argv)
   -1   162     except SyntaxError as e:
   -1   163         print(f'{e.filename}:{e.lineno}: {e.msg}', file=sys.stderr)
   -1   164         sys.exit(1)
   -1   165     except ValueError:
   -1   166         print(USAGE)
   -1   167         sys.exit(1)
   -1   168     cmd = rules.build(tail)
   -1   169     if rules.usage:
   -1   170         print(USAGE)
   -1   171     elif rules.debug:
   -1   172         print(' '.join(cmd))
   -1   173     else:
   -1   174         os.execvp('/usr/bin/bwrap', cmd)