ybox 0.9.10__py3-none-any.whl → 0.9.11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ybox/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
1
  """`ybox` is a tool to easily manage linux distributions in containers"""
2
- __version__ = "0.9.10"
2
+ __version__ = "0.9.11"
@@ -1,6 +1,7 @@
1
1
  [base]
2
2
  name = Profile for CLI and GUI apps
3
3
  includes = basic.ini
4
+ ssh_agent = on
4
5
 
5
6
  [security]
6
7
  # SYS_PTRACE may be required by mesa which is invoked indirectly by both firefox and chromium.
@@ -9,6 +10,9 @@ includes = basic.ini
9
10
  caps_add = SYS_PTRACE
10
11
 
11
12
  [mounts]
13
+ # export the host's ssh keys for use by ssh-agent in the container as required ("ro" mode
14
+ # implies that known_hosts and other files within ~/.ssh cannot be changed)
15
+ ssh = $HOME/.ssh:$TARGET_HOME/.ssh:ro
12
16
  music = $HOME/Music:$TARGET_HOME/Music:ro
13
17
  pictures = $HOME/Pictures:$TARGET_HOME/Pictures:ro
14
18
  videos = $HOME/Videos:$TARGET_HOME/Videos:ro
@@ -19,8 +23,9 @@ videos = $HOME/Videos:$TARGET_HOME/Videos:ro
19
23
 
20
24
  [app_flags]
21
25
  # These flags will be added to Exec line of google-chrome.desktop when it is copied to host.
22
- # /dev/shm usage is disabled for chrome because that requires ipc=host or mounting host
23
- # /dev/shm in read-write mode which can be insecure.
24
- google-chrome = !p --disable-dev-shm-usage !a
25
- google-chrome-beta = !p --disable-dev-shm-usage !a
26
- google-chrome-unstable = !p --disable-dev-shm-usage !a
26
+
27
+ # the --disable-dev-shm-usage flag in chrome/chromium based browsers disables use of /dev/shm
28
+ # which can reduce memory footprint at the cost of performance and increased disk activity
29
+ #google-chrome = !p --disable-dev-shm-usage !a
30
+ #google-chrome-beta = !p --disable-dev-shm-usage !a
31
+ #google-chrome-unstable = !p --disable-dev-shm-usage !a
@@ -20,8 +20,8 @@
20
20
  # - YBOX_SYS_CONF_DIR: path to system configuration directory where configuration directory
21
21
  # shipped with ybox is installed (or the string form of
22
22
  # the directory if it is not on filesystem like an egg or similar)
23
- # - TARGET_HOME: set to the home directory of the container user as it exists on the host
24
- # (i.e. the expanded value of the "home" key in the [base] section)
23
+ # - TARGET_HOME: set to the home directory of the container user in the container
24
+ # (which is same as the host user's $HOME for podman and /root for docker)
25
25
  # Additionally a special notation can be used for current date+time with this notation:
26
26
  # ${NOW:<fmt>}. The <fmt> uses the format supported by python strftime
27
27
  # (https://docs.python.org/3/library/datetime.html#datetime.datetime.strftime)
@@ -67,7 +67,7 @@ includes =
67
67
  # to freely create as many containers as desired to achieve best isolation without worrying
68
68
  # about dramatic increase in disk and/or memory usage.
69
69
  shared_root = $HOME/.local/share/ybox/SHARED_ROOTS/$YBOX_DISTRIBUTION_NAME
70
- # Bind mount the container $HOME to this local path (aka $TARGET_HOME). This makes it
70
+ # Bind mount the container $HOME to this local path. This makes it
71
71
  # easier for backup software and otherwise to read useful container data.
72
72
  # If not provided then you should explicitly mount required directories in the [mounts]
73
73
  # section otherwise home will remain completely ephemeral which is not recommended.
@@ -100,6 +100,16 @@ pulseaudio = on
100
100
  dbus = on
101
101
  # If enabled then the system dbus from the host is available to the container.
102
102
  dbus_sys = off
103
+ # If enabled then the socket for SSH agent, if present, is made available to the container.
104
+ # The $SSH_AUTH_SOCK environment variables must be set in the host environment for this to work.
105
+ # You can also mount $HOME/.ssh with appropriate flags ("ro" if possible) in the [mounts]
106
+ # section to enable the container use the host's ssh keys.
107
+ ssh_agent = off
108
+ # If enabled then the socket for GPG agent, if present, is made available to the container.
109
+ # The $GPG_AGENT_INFO environment variable must be set in the host environment for this to work.
110
+ # You can also mount $HOME/.gnupg with appropriate flags ("ro" if possible) in the [mounts]
111
+ # section to enable the container use the host's gpg keys.
112
+ gpg_agent = off
103
113
  # If enabled then Direct Rendering Infrastructure for accelerated graphics is available to
104
114
  # the container.
105
115
  dri = on
@@ -123,8 +133,8 @@ nvidia = off
123
133
  #
124
134
  # This will take precedence if both "nvidia" and "nvidia_ctk" are enabled.
125
135
  nvidia_ctk = off
126
- # default podman/docker shm-size is 64m which can be insufficient for many apps
127
- shm_size = 1g
136
+ # default podman/docker shm-size is only 64m which can be insufficient for many apps
137
+ shm_size = 2g
128
138
  # Limit the maximum number of processes in the container (to avoid stuff like fork bombs).
129
139
  pids_limit = 2048
130
140
  # Logging driver to use. Default for podman/docker is to use journald in modern Linux
@@ -137,6 +147,9 @@ log_driver = json-file
137
147
  # Example for docker that does not support `path`
138
148
  log_opts = max-size=10m,max-file=3
139
149
 
150
+ # Comma separated list of additional devices that should be made available to the container using
151
+ # the --device option to podman/docker run. Example: devices = /dev/video0,/dev/ttyUSB0
152
+ devices =
140
153
 
141
154
  # The security-opt and other security options passed to podman/docker.
142
155
  # You should restrict these as required.
@@ -227,9 +240,10 @@ documents = $HOME/Documents:$TARGET_HOME/Documents:ro
227
240
  # in the [base] section.
228
241
  #
229
242
  # Note: The LHS should typically have a path having $HOME while RHS will be relative to the
230
- # target's home inside the container. Do not use $TARGET_HOME on RHS (or LHS for that matter)
231
- # which is the target's home as on the host and not the one inside the container.
243
+ # target's home inside the container. Do not use $TARGET_HOME on RHS since path the assumed
244
+ # to be a relative one and $TARGET_HOME already inserted as required.
232
245
  [configs]
246
+ env_conf = $HOME/.config/environment.d -> .config/environment.d
233
247
  bashrc = $HOME/.bashrc -> .bashrc
234
248
  starship = $HOME/.config/starship.toml -> .config/starship.toml
235
249
  # replicate fish configuration directory with copy of fish_variables but symlinks for the rest
@@ -303,14 +317,12 @@ XMODIFIERS
303
317
  [app_flags]
304
318
  # These flags/arguments will be added to Exec line of chromium.desktop when it is copied to
305
319
  # host as well as in the wrapper chromium executable created on the host.
306
- # You can use "!p" here for the first argument in the 'Exec='/'TryExec=' line in the desktop
320
+ # You can use "!p" here for the first argument in the 'Exec=' line in the desktop
307
321
  # file and '!a' for rest of the arguments. When linking to an executable program, '!p' will
308
322
  # refer to the full path of the executable while '!a' will be replaced by "$@" in the shell
309
323
  # script. Use '!!p' for a literal '!p' and '!!a' for a literal '!a'.
310
324
 
311
- # /dev/shm usage is disabled for chromium because that requires ipc=host or mounting host
312
- # /dev/shm in read-write mode which is quite insecure.
313
- chromium = !p --disable-dev-shm-usage --enable-chrome-browser-cloud-management !a
325
+ chromium = !p --enable-chrome-browser-cloud-management !a
314
326
 
315
327
 
316
328
  # Startup programs you want to run when starting the container. These are run using
@@ -1,6 +1,7 @@
1
1
  [base]
2
2
  name = Profile for creating development environment
3
3
  includes = basic.ini
4
+ ssh_agent = on
4
5
 
5
6
  [security]
6
7
  # SYS_PTRACE is required by mesa and without this, the following warning can be seen:
@@ -8,6 +9,9 @@ includes = basic.ini
8
9
  caps_add = SYS_PTRACE
9
10
 
10
11
  [mounts]
12
+ # export the host's ssh keys for use by ssh-agent in the container as required ("ro" mode
13
+ # implies that known_hosts and other files within ~/.ssh cannot be changed)
14
+ ssh = $HOME/.ssh:$TARGET_HOME/.ssh:ro
11
15
  # add your projects and other directories having source code
12
16
  #projects = $HOME/projects:$TARGET_HOME/projects
13
17
  #pyenv = $HOME/.pyenv:$TARGET_HOME/.pyenv:ro
@@ -28,5 +28,5 @@ echo_color "$fg_purple" "Copying data from container to shared root mounted on '
28
28
  IFS="," read -ra shared_dirs_arr <<< "$shared_dirs"
29
29
  for dir in "${shared_dirs_arr[@]}"; do
30
30
  echo_color "$fg_orange" "Copying $dir to $shared_bind$dir"
31
- cp -a "$dir" "$shared_bind$dir"
31
+ cp -an "$dir" "$shared_bind$dir"
32
32
  done
@@ -10,9 +10,9 @@ source "$SCRIPT_DIR/entrypoint-common.sh"
10
10
 
11
11
  export HOME=/root
12
12
  echo_color "$fg_cyan" "Copying prime-run, run-in-dir and run-user-bash-cmd" >> $status_file
13
- cp -a "$SCRIPT_DIR/prime-run" /usr/local/bin/prime-run
14
- cp -a "$SCRIPT_DIR/run-in-dir" /usr/local/bin/run-in-dir
15
- cp -a "$SCRIPT_DIR/run-user-bash-cmd" /usr/local/bin/run-user-bash-cmd
13
+ cp -af "$SCRIPT_DIR/prime-run" /usr/local/bin/prime-run
14
+ cp -af "$SCRIPT_DIR/run-in-dir" /usr/local/bin/run-in-dir
15
+ cp -af "$SCRIPT_DIR/run-user-bash-cmd" /usr/local/bin/run-user-bash-cmd
16
16
  chmod 0755 /usr/local/bin/prime-run /usr/local/bin/run-in-dir /usr/local/bin/run-user-bash-cmd
17
17
 
18
18
  # invoke the NVIDIA setup script if present
@@ -57,19 +57,12 @@ function replicate_config_files() {
57
57
  home_file="$HOME/${BASH_REMATCH[2]}"
58
58
  dest_file="$config_dir/${BASH_REMATCH[2]}"
59
59
  # only replace the file if it is already a link (assuming the link target may
60
- # have changed in the config_list file), or a directory containing links
60
+ # have changed in the config_list file), or a directory containing only links
61
61
  if [ -e "$dest_file" ]; then
62
62
  if [ -L "$home_file" ]; then
63
63
  rm -f "$home_file"
64
64
  elif [ -d "$home_file" ]; then
65
- do_rmdir=true
66
- for f in "$home_file"/*; do
67
- if [ -e "$f" -a ! -L "$f" ]; then
68
- do_rmdir=false
69
- break
70
- fi
71
- done
72
- if [ "$do_rmdir" = true ]; then
65
+ if [ -z $(find "$home_file" -type f -print -quit) ]; then
73
66
  rm -rf "$home_file"
74
67
  fi
75
68
  fi
@@ -9,24 +9,34 @@ if [ -n "$dir" -a -d "$dir" ]; then
9
9
  cd "$dir"
10
10
  fi
11
11
 
12
- # XAUTHORITY file can change after a re-login or a restart, so search for the passed one
13
- # by podman/docker exec in the mount point of its parent directory
14
- if [ -n "$XAUTHORITY" -a -n "$XAUTHORITY_ORIG" -a ! -r "$XAUTHORITY" ]; then
15
- # XAUTHORITY is assumed to be either in /run/user or in /tmp
16
- run_dir="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
17
- if [[ "$XAUTHORITY" == $run_dir/* ]]; then
18
- host_dir="$run_dir"
19
- elif [[ "$XAUTHORITY" == /tmp/* ]]; then
20
- host_dir=/tmp
12
+ # XAUTHORITY, SSH_AUTH_SOCK and GPG_AGENT_INFO files can change after a re-login or a restart,
13
+ # so search for the passed one by podman/docker exec in the mount point of its parent directory
14
+ for env_var in XAUTHORITY SSH_AUTH_SOCK GPG_AGENT_INFO; do
15
+ env_var_orig=${env_var}_ORIG
16
+ var_val=${!env_var}
17
+ var_val_orig=${!env_var_orig}
18
+ if [ -n "$var_val" -a -n "$var_val_orig" ]; then
19
+ if [ ! -r "$var_val" ]; then
20
+ # the value should be in /run/user/<uid> or in /tmp, or else the parent directory is used
21
+ run_dir="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
22
+ if [[ "$var_val" == $run_dir/* ]]; then
23
+ host_dir="$run_dir"
24
+ elif [[ "$var_val" == /tmp/* ]]; then
25
+ host_dir=/tmp
26
+ else
27
+ host_dir="$(dirname "$var_val")"
28
+ fi
29
+ new_val="${var_val/#$host_dir/${host_dir}-host}" # replace $host_dir by ${host_dir}-host
30
+ if [ ! -r "$new_val" ]; then
31
+ new_val="$var_val_orig"
32
+ fi
33
+ export $env_var="$new_val"
34
+ fi
21
35
  else
22
- host_dir="$(dirname "$XAUTHORITY")"
36
+ # remove unset variable in the container else apps can misbehave
37
+ unset $env_var
23
38
  fi
24
- XAUTHORITY="${XAUTHORITY/#$host_dir/${host_dir}-host}" # replace $host_dir by ${host_dir}-host
25
- if [ ! -r "$XAUTHORITY" ]; then
26
- XAUTHORITY="$XAUTHORITY_ORIG"
27
- fi
28
- export XAUTHORITY
29
- fi
39
+ done
30
40
 
31
41
  # In case NVIDIA driver has been updated, the updated libraries and other files may need to be
32
42
  # linked again, so check for a missing library file and invoke the setup script if present
@@ -8,15 +8,17 @@ Wants=network-online.target
8
8
  After=network-online.target
9
9
  {docker_requires}
10
10
  [Service]
11
- EnvironmentFile={env_file}
12
- Type=forking
11
+ Environment=PATH={sys_path}:{ybox_bin_dir}
12
+ EnvironmentFile=%h/.config/systemd/user/{env_file}
13
+ Type=notify
14
+ NotifyAccess=all
13
15
  Restart=on-failure
14
- TimeoutStopSec=70
15
16
  # sleep to allow for initialization of the user's login/graphical environment
16
17
  ExecStartPre=/usr/bin/sleep $SLEEP_SECS
17
- ExecStart=/bin/sh -c 'ybox-control start {name}'
18
+ ExecStart=/bin/sh -c 'ybox-control start {name} && systemd-notify --ready && exec ybox-control wait {name}'
18
19
  ExecStop=/bin/sh -c 'ybox-control stop -t 20 --ignore-stopped {name}'
19
20
  ExecStopPost=/bin/sh -c 'ybox-control stop -t 20 --ignore-stopped {name}'
20
- {pid_file}
21
+ TimeoutStopSec=60
22
+
21
23
  [Install]
22
24
  WantedBy=default.target
ybox/env.py CHANGED
@@ -60,34 +60,41 @@ class Environ:
60
60
  :param home_dir: if a non-default user home directory has to be set
61
61
  """
62
62
  self._home_dir = home_dir or os.path.expanduser("~")
63
+ self._home_dir = self._home_dir.rstrip("/")
63
64
  self._docker_cmd = docker_cmd or get_docker_command()
64
65
  cmd_version = subprocess.check_output([self._docker_cmd, "--version"])
65
66
  self._uses_podman = "podman" in cmd_version.decode("utf-8").lower()
66
67
  # local user home might be in a different location than /home but target user in the
67
68
  # container will always be in /home with podman else /root for the root user with docker
68
69
  # as ensured by entrypoint-base.sh script
69
- target_uid = 0
70
+ current_user = getpass.getuser()
71
+ current_uid = pwd.getpwnam(current_user).pw_uid
70
72
  if self._uses_podman:
71
- self._target_user = getpass.getuser()
72
- target_uid = pwd.getpwnam(self._target_user).pw_uid
73
+ self._target_user = current_user
74
+ target_uid = current_uid
73
75
  self._target_home = f"/home/{self._target_user}"
74
76
  else:
75
77
  self._target_user = "root"
78
+ target_uid = 0
76
79
  self._target_home = "/root"
77
80
  # confirm that docker is being used in rootless mode (not required for podman because
78
81
  # it runs as rootless when run by a non-root user in any case without explicit sudo
79
82
  # which the ybox tools don't use)
80
83
  if (docker_ctx := subprocess.check_output(
81
84
  [self._docker_cmd, "context", "show"]).decode("utf-8")).strip() != "rootless":
82
- raise NotSupportedError("docker should use the rootless mode (see "
83
- "https://docs.docker.com/engine/security/rootless/) "
84
- f"but the current context is '{docker_ctx}'")
85
+ # check for DOCKER_HOST environment variable
86
+ expected_docker_host = f"unix:///run/user/{current_uid}/docker.sock"
87
+ if not docker_ctx or os.environ.get("DOCKER_HOST", "") != expected_docker_host:
88
+ raise NotSupportedError("docker should use the rootless mode (see "
89
+ "https://docs.docker.com/engine/security/rootless/) "
90
+ f"but the current context is '{docker_ctx}' and "
91
+ f"$DOCKER_HOST is not set to '{expected_docker_host}'")
85
92
  os.environ["TARGET_HOME"] = self._target_home
86
93
  self._user_base = user_base = site.getuserbase()
87
94
  target_user_base = f"{self._target_home}/.local"
88
95
  self._data_dir = f"{user_base}/share/ybox"
89
96
  self._target_data_dir = f"{target_user_base}/share/ybox"
90
- self._xdg_rt_dir = os.environ.get("XDG_RUNTIME_DIR", "")
97
+ self._xdg_rt_dir = os.environ.get("XDG_RUNTIME_DIR", "").rstrip("/")
91
98
  # the container user's one can be different because it is the root user for docker
92
99
  self._target_xdg_rt_dir = f"/run/user/{target_uid}"
93
100
  self._now = datetime.now()
@@ -148,6 +155,10 @@ class Environ:
148
155
  """if podman is the container manager being used"""
149
156
  return self._uses_podman
150
157
 
158
+ def systemd_user_conf_dir(self) -> str:
159
+ """standard configuration directory location of user specific systemd services"""
160
+ return f"{self._home_dir}/.config/systemd/user"
161
+
151
162
  @property
152
163
  def target_user(self) -> str:
153
164
  """username of the container user (which is the same as the current user for podman
@@ -20,11 +20,12 @@ copy_ybox_scripts_to_container(static_conf, distro_conf)
20
20
 
21
21
  # rename PKGMGR_CLEANUP to PKGMGR_CLEAN in pkgmgr.conf
22
22
  scripts_dir = static_conf.scripts_dir
23
- pkgmgr_conf = f"{scripts_dir}/pkgmgr.conf"
24
- with open(pkgmgr_conf, "r", encoding="utf-8") as pkgmgr_file:
25
- pkgmgr_data = pkgmgr_file.read()
26
- with open(pkgmgr_conf, "w", encoding="utf-8") as pkgmgr_file:
27
- pkgmgr_file.write(pkgmgr_data.replace("PKGMGR_CLEANUP", "PKGMGR_CLEAN"))
23
+ pkgmgr_conf = Path(f"{scripts_dir}/pkgmgr.conf")
24
+ if pkgmgr_conf.exists():
25
+ with pkgmgr_conf.open("r", encoding="utf-8") as pkgmgr_file:
26
+ pkgmgr_data = pkgmgr_file.read()
27
+ with pkgmgr_conf.open("w", encoding="utf-8") as pkgmgr_file:
28
+ pkgmgr_file.write(pkgmgr_data.replace("PKGMGR_CLEANUP", "PKGMGR_CLEAN"))
28
29
  # run entrypoint-root.sh again to refresh scripts and configuration
29
30
  subprocess.run([static_conf.env.docker_cmd, "exec", "-it", static_conf.box_name, "/usr/bin/sudo",
30
31
  "/bin/bash", f"{static_conf.target_scripts_dir}/entrypoint-root.sh"])
ybox/pkg/inst.py CHANGED
@@ -24,11 +24,16 @@ from ybox.state import (CopyType, DependencyType, RuntimeConfiguration,
24
24
  from ybox.util import check_package, ini_file_reader, select_item_from_menu
25
25
 
26
26
  # match both "Exec=" and "TryExec=" lines (don't capture trailing newline)
27
- _EXEC_RE = re.compile(r"^(\s*(Try)?Exec\s*=\s*)(\S+)\s*(.*?)\s*$")
27
+ _EXEC_PATTERN = r"\s*((Try)?Exec\s*=\s*)(\S+)\s*(.*?)\s*"
28
+ # pattern to match icons with absolute paths and change them to just names
29
+ _ICON_PATH_PATTERN = r"\s*Icon\s*=\s*(/usr/share/(icons|pixmaps)/\S+)\s*"
30
+ # regex to match either of the two above
31
+ _EXEC_ICON_RE = re.compile(f"^{_EXEC_PATTERN}|{_ICON_PATH_PATTERN}$")
28
32
  # match !p and !a to replace executable program (third group above) and arguments respectively
29
33
  _FLAGS_RE = re.compile("![ap]")
30
34
  # environment variables passed through from host environment to podman/docker executable
31
35
  _PASSTHROUGH_ENVVARS = ("XAUTHORITY", "DISPLAY", "WAYLAND_DISPLAY", "FREETYPE_PROPERTIES",
36
+ "SSH_AUTH_SOCK", "GPG_AGENT_INFO",
32
37
  "__NV_PRIME_RENDER_OFFLOAD", "__GLX_VENDOR_LIBRARY_NAME",
33
38
  "__VK_LAYER_NV_optimus", "VK_ICD_FILES", "VK_ICD_FILENAMES")
34
39
 
@@ -156,7 +161,8 @@ def _install_package(package: str, args: argparse.Namespace, install_cmd: str, l
156
161
  if not skip_desktop_files:
157
162
  copy_type |= CopyType.DESKTOP
158
163
  if not skip_executables:
159
- resp = input("Create application executable(s)? (Y/n) ") if quiet == 0 else "Y"
164
+ resp = input("Create wrapper(s) for application executable(s) of package "
165
+ f"'{package}'? (Y/n) ") if quiet == 0 else "Y"
160
166
  if resp.strip().lower() != "n":
161
167
  copy_type |= CopyType.EXECUTABLE
162
168
  # TODO: wrappers for newly installed required dependencies should also be created;
@@ -436,9 +442,9 @@ def docker_cp_action(docker_cmd: str, box_name: str, src: str,
436
442
  def _wrap_desktop_file(filename: str, file: str, docker_cmd: str, conf: StaticConfiguration,
437
443
  app_flags: dict[str, str], wrapper_files: list[str]) -> None:
438
444
  """
439
- For a desktop file, add "podman/docker exec ..." to its Exec/TryExec lines. Also read
440
- the additional flags for the command passed in `app_flags` and add them to an appropriate
441
- position in the Exec/TryExec lines.
445
+ For a desktop file, add "podman/docker exec ..." to its `Exec` lines. Also read the additional
446
+ flags for the command passed in `app_flags` and add them to an appropriate position in the
447
+ `Exec` lines. Also removes the `TryExec` lines that do not work in some desktop environments.
442
448
 
443
449
  :param filename: name of the desktop file being wrapped
444
450
  :param file: full path of the desktop file being wrapped
@@ -451,7 +457,13 @@ def _wrap_desktop_file(filename: str, file: str, docker_cmd: str, conf: StaticCo
451
457
  # container name is added to desktop file to make it unique
452
458
  wrapper_name = f"ybox.{conf.box_name}.{filename}"
453
459
 
454
- def replace_executable(match: re.Match[str]) -> str:
460
+ def replace_exec_icon(match: re.Match[str]) -> str:
461
+ """replace Exec, TryExec and Icon lines appropriately for the host system"""
462
+ if not (exec_word := match.group(1)): # check for the case of `Icon=/usr/...`
463
+ return f"Icon={os.path.basename(match.group(5))}\n"
464
+ # remove TryExec lines that don't work in some desktop environments (like KDE plasma 5)
465
+ if match.group(2):
466
+ return ""
455
467
  program = match.group(3)
456
468
  args = match.group(4)
457
469
  # check for additional flags to be added
@@ -464,7 +476,7 @@ def _wrap_desktop_file(filename: str, file: str, docker_cmd: str, conf: StaticCo
464
476
  full_cmd = program
465
477
  # pseudo-tty cannot be allocated with rootless docker outside of a terminal app
466
478
  env_vars = " -e=".join(_PASSTHROUGH_ENVVARS)
467
- return (f'{match.group(1)}{docker_cmd} exec -e={env_vars} {conf.box_name} '
479
+ return (f'{exec_word}{docker_cmd} exec -e={env_vars} {conf.box_name} '
468
480
  f'/usr/local/bin/run-in-dir "" {full_cmd}\n')
469
481
 
470
482
  # the destination will be $HOME/.local/share/applications
@@ -476,7 +488,7 @@ def _wrap_desktop_file(filename: str, file: str, docker_cmd: str, conf: StaticCo
476
488
  def write_desktop_file(src: str) -> None:
477
489
  with open(wrapper_file, "w", encoding="utf-8") as wrapper_fd:
478
490
  with open(src, "r", encoding="utf-8") as src_fd:
479
- wrapper_fd.writelines(_EXEC_RE.sub(replace_executable, line) for line in src_fd)
491
+ wrapper_fd.writelines(_EXEC_ICON_RE.sub(replace_exec_icon, line) for line in src_fd)
480
492
  if docker_cp_action(docker_cmd, conf.box_name, file, write_desktop_file) == 0:
481
493
  wrapper_files.append(wrapper_file)
482
494
 
@@ -545,8 +557,8 @@ def _copy_app_icons(selected_icons: dict[str, tuple[float, str]], docker_cmd: st
545
557
  # copy from temporary file over the existing one, if any, to overwrite rather than move
546
558
  # (which will preserve all of its hard links, for example)
547
559
  exists = os.path.exists(target_icon_path)
548
- if docker_cp_action(docker_cmd, conf.box_name, icon_path,
549
- lambda src, dest=target_icon_path: shutil.copy2(src, dest)) == 0:
560
+ if docker_cp_action(docker_cmd, conf.box_name, icon_path, lambda src, dest=target_icon_path:
561
+ None if shutil.copy2(src, dest) else None) == 0: # "if" for pyright
550
562
  # skip registration of icon file it already existed and was overwritten so that
551
563
  # it is not removed on package uninstall
552
564
  if not exists:
ybox/pkg/mark.py CHANGED
@@ -30,7 +30,7 @@ def mark_package(args: argparse.Namespace, pkgmgr: SectionProxy, docker_cmd: str
30
30
  mark_explicit: bool = args.explicit
31
31
  mark_dependency_of: str = args.dependency_of or ""
32
32
  if not mark_explicit ^ bool(mark_dependency_of):
33
- print_error("ybox-pkg mark: exactly one of -e or -D option must be specified "
33
+ print_error("ybox-pkg mark: exactly one of -e or -d option must be specified "
34
34
  f"(explicit={mark_explicit}, dependency-of={mark_dependency_of})")
35
35
  return 1
36
36
  # check that the package(s) are installed and replace with actual installed name
ybox/run/control.py CHANGED
@@ -32,7 +32,7 @@ def start_container(docker_cmd: str, args: argparse.Namespace):
32
32
  container_name = args.container
33
33
  if status := get_ybox_state(docker_cmd, container_name, (), exit_on_error=False):
34
34
  if status[0] == "running":
35
- print_color(f"Ybox container '{container_name}' already active", fg=fgcolor.cyan)
35
+ print_color(f"ybox container '{container_name}' already active", fg=fgcolor.cyan)
36
36
  else:
37
37
  print_color(f"Starting ybox container '{container_name}'", fg=fgcolor.cyan)
38
38
  run_command([docker_cmd, "container", "start", container_name],
@@ -107,6 +107,17 @@ def show_container_status(docker_cmd: str, args: argparse.Namespace) -> None:
107
107
  print_error(f"No ybox container '{container_name}' found")
108
108
 
109
109
 
110
+ def wait_for_container_stop(docker_cmd: str, args: argparse.Namespace) -> None:
111
+ """
112
+ Wait for an active container to stop.
113
+
114
+ :param docker_cmd: the podman/docker executable to use
115
+ :param args: arguments having all attributes passed by the user
116
+ """
117
+ while check_active_ybox(docker_cmd, args.container):
118
+ time.sleep(2)
119
+
120
+
110
121
  def main_argv(argv: list[str]) -> None:
111
122
  """
112
123
  Main entrypoint of `ybox-control` that takes a list of arguments which are usually the
@@ -138,7 +149,7 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
138
149
  stop = operations.add_parser("stop", help="stop a ybox container")
139
150
  _add_subparser_args(stop, 10,
140
151
  "time in seconds to wait for a container to stop before killing it")
141
- stop.add_argument("--ignore-stopped", action="store_true",
152
+ stop.add_argument("-I", "--ignore-stopped", action="store_true",
142
153
  help="don't fail on an already stopped container")
143
154
  stop.set_defaults(func=stop_container)
144
155
 
@@ -150,6 +161,10 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
150
161
  _add_subparser_args(status, 0, "")
151
162
  status.set_defaults(func=show_container_status)
152
163
 
164
+ wait = operations.add_parser("wait", help="wait for an active ybox container to stop")
165
+ _add_subparser_args(wait, 0, "")
166
+ wait.set_defaults(func=wait_for_container_stop)
167
+
153
168
  parser_version_check(parser, argv)
154
169
  return parser.parse_args(argv)
155
170
 
ybox/run/create.py CHANGED
@@ -27,9 +27,11 @@ from ybox.filelock import FileLock
27
27
  from ybox.pkg.inst import install_package, wrap_container_files
28
28
  from ybox.print import (bgcolor, fgcolor, print_color, print_error, print_info,
29
29
  print_notice, print_warn)
30
- from ybox.run.destroy import get_all_containers, remove_orphans_from_db
30
+ from ybox.run.destroy import (get_all_containers, remove_orphans_from_db,
31
+ ybox_systemd_service_prefix)
31
32
  from ybox.run.graphics import (add_env_option, add_mount_option, enable_dri,
32
- enable_nvidia, enable_wayland, enable_x11)
33
+ enable_nvidia, enable_wayland, enable_x11,
34
+ handle_variable_mount)
33
35
  from ybox.run.pkg import parse_args as pkg_parse_args
34
36
  from ybox.state import RuntimeConfiguration, YboxStateManagement
35
37
  from ybox.util import (EnvInterpolation, config_reader,
@@ -195,10 +197,16 @@ def main_argv(argv: list[str]) -> None:
195
197
  Path(f"{conf.scripts_dir}/{Consts.entrypoint_init_done_file()}").touch(mode=0o644)
196
198
  wait_msg = ("Waiting for the container to be ready "
197
199
  f"(see ybox-logs -f {box_name}' for detailed progress)")
198
- if args.systemd_service and (sys_path := os.pathsep.join(Consts.sys_bin_dirs())) and (
199
- systemctl := shutil.which("systemctl", path=sys_path)):
200
+ if not args.skip_systemd_service and (sys_path := os.pathsep.join(Consts.sys_bin_dirs())) and (
201
+ systemctl := shutil.which("systemctl", path=sys_path)) and run_command(
202
+ [systemctl, "--user", "--quiet", "is-enabled", "default.target"],
203
+ exit_on_error=False) == 0:
200
204
  create_and_start_service(box_name, env, systemctl, sys_path, wait_msg)
201
205
  else:
206
+ if not args.skip_systemd_service:
207
+ print_warn("Skipping user systemd service generation due to missing systemctl in "
208
+ f"PATH={os.pathsep.join(Consts.sys_bin_dirs())} or failure in "
209
+ "'systemctl --user is-enabled default.target'")
202
210
  start_container(docker_cmd, conf)
203
211
  print_info(wait_msg)
204
212
  wait_for_ybox_container(docker_cmd, conf, 120)
@@ -268,9 +276,12 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
268
276
  parser.add_argument("-n", "--name", type=str,
269
277
  help="name of the ybox; default is ybox-<distribution>_<profile> "
270
278
  "if not provided (removing the .ini suffix from <profile> file)")
271
- parser.add_argument("-S", "--systemd-service", action="store_true",
272
- help="create/overwrite user systemd service file for the container in "
273
- "~/.config/systemd/user and enable it by default")
279
+ parser.add_argument("-S", "--skip-systemd-service", action="store_true",
280
+ help="skip creation of user systemd service file for the ybox container; "
281
+ "by default a user systemd service file is created and enabled in "
282
+ "~/.config/systemd/user with the name 'ybox-<name>.service' if the "
283
+ "<name> does not begin with 'ybox-' prefix else '<name>.service' if "
284
+ "it already has 'ybox-' prefix")
274
285
  parser.add_argument("-F", "--force-own-orphans", action="store_true",
275
286
  help="force ownership of orphan packages on the same shared root even "
276
287
  "if container configuration does not match, meaning the packages "
@@ -607,6 +618,12 @@ def process_base_section(base_section: SectionProxy, profile: PathName, conf: St
607
618
  elif key == "dbus":
608
619
  if _get_boolean(val):
609
620
  enable_dbus(docker_args, base_section.getboolean("dbus_sys", fallback=False), env)
621
+ elif key == "ssh_agent":
622
+ if _get_boolean(val):
623
+ enable_ssh_agent(docker_args, env)
624
+ elif key == "gpg_agent":
625
+ if _get_boolean(val):
626
+ enable_gpg_agent(docker_args, env)
610
627
  elif key == "dri":
611
628
  dri = _get_boolean(val)
612
629
  elif key == "nvidia":
@@ -629,6 +646,9 @@ def process_base_section(base_section: SectionProxy, profile: PathName, conf: St
629
646
  (re.match("^--log-opt=path=(.*)/.*$", path) for path in docker_args) if mt]
630
647
  for log_dir in log_dirs:
631
648
  os.makedirs(log_dir, mode=Consts.default_directory_mode(), exist_ok=True)
649
+ elif key == "devices":
650
+ if val:
651
+ add_multi_opt(docker_args, "device", val)
632
652
  elif key not in ("name", "dbus_sys", "includes"):
633
653
  raise NotSupportedError(f"Unknown key '{key}' in the [base] of {profile} "
634
654
  "or its includes")
@@ -685,21 +705,51 @@ def enable_dbus(docker_args: list[str], sys_enable: bool, env: Environ) -> None:
685
705
  to the user dbus message bus
686
706
  :param env: an instance of the current :class:`Environ`
687
707
  """
688
- def replace_target_dir(src: str) -> str:
689
- return src.replace(f"{env.xdg_rt_dir}/", f"{env.target_xdg_rt_dir}/")
690
708
  if dbus_session := os.environ.get("DBUS_SESSION_BUS_ADDRESS"):
691
709
  dbus_user = dbus_session[dbus_session.find("=") + 1:]
692
710
  if (dbus_opts_idx := dbus_user.find(",")) != -1:
693
711
  dbus_user = dbus_user[:dbus_opts_idx]
694
- add_mount_option(docker_args, dbus_user, replace_target_dir(dbus_user))
695
- add_env_option(docker_args, "DBUS_SESSION_BUS_ADDRESS", replace_target_dir(dbus_session))
712
+ add_mount_option(docker_args, dbus_user, _replace_xdg_rt_dir(dbus_user, env))
713
+ add_env_option(docker_args, "DBUS_SESSION_BUS_ADDRESS",
714
+ _replace_xdg_rt_dir(dbus_session, env))
696
715
  if sys_enable:
697
- dbus_sys = "/run/dbus/system_bus_socket"
698
- dbus_sys2 = "/var/run/dbus/system_bus_socket"
699
- if os.access(dbus_sys, os.W_OK):
700
- add_mount_option(docker_args, dbus_sys, dbus_sys)
701
- elif os.access(dbus_sys2, os.W_OK):
702
- add_mount_option(docker_args, dbus_sys2, dbus_sys)
716
+ for dbus_sys in ("/run/dbus/system_bus_socket", "/var/run/dbus/system_bus_socket"):
717
+ if os.access(dbus_sys, os.W_OK):
718
+ add_mount_option(docker_args, dbus_sys, dbus_sys)
719
+ break
720
+
721
+
722
+ def enable_ssh_agent(docker_args: list[str], env: Environ) -> None:
723
+ """
724
+ Append options to podman/docker arguments to share host machine's ssh agent socket
725
+ with the new ybox container.
726
+
727
+ :param docker_args: list of podman/docker arguments to which the options have to be appended
728
+ :param env: an instance of the current :class:`Environ`
729
+ """
730
+ if ssh_auth_sock := os.environ.get("SSH_AUTH_SOCK"):
731
+ target_ssh_auth_sock = handle_variable_mount(docker_args, env, ssh_auth_sock)
732
+ add_env_option(docker_args, "SSH_AUTH_SOCK", target_ssh_auth_sock)
733
+ add_env_option(docker_args, "SSH_AUTH_SOCK_ORIG", target_ssh_auth_sock)
734
+
735
+
736
+ def enable_gpg_agent(docker_args: list[str], env: Environ) -> None:
737
+ """
738
+ Append options to podman/docker arguments to share host machine's gpg agent sockets
739
+ with the new ybox container.
740
+
741
+ :param docker_args: list of podman/docker arguments to which the options have to be appended
742
+ :param env: an instance of the current :class:`Environ`
743
+ """
744
+ if gpg_agent_info := os.environ.get("GPG_AGENT_INFO"):
745
+ target_gpg_agent_info = handle_variable_mount(docker_args, env, gpg_agent_info)
746
+ add_env_option(docker_args, "GPG_AGENT_INFO", target_gpg_agent_info)
747
+ add_env_option(docker_args, "GPG_AGENT_INFO_ORIG", target_gpg_agent_info)
748
+
749
+
750
+ def _replace_xdg_rt_dir(src: str, env: Environ) -> str:
751
+ """replace host's $XDG_RUNTIME_DIR in `src` with that of container user's $XDG_RUNTIME_DIR"""
752
+ return src.replace(env.xdg_rt_dir + "/", env.target_xdg_rt_dir + "/")
703
753
 
704
754
 
705
755
  def add_multi_opt(docker_args: list[str], opt: str, val: Optional[str]) -> None:
@@ -712,7 +762,7 @@ def add_multi_opt(docker_args: list[str], opt: str, val: Optional[str]) -> None:
712
762
  """
713
763
  if val:
714
764
  for opt_val in val.split(","):
715
- docker_args.append(f"--{opt}={opt_val}")
765
+ docker_args.append(f"--{opt}={opt_val.strip()}")
716
766
 
717
767
 
718
768
  def process_security_section(sec_section: SectionProxy, profile: PathName,
@@ -790,8 +840,7 @@ def process_configs_section(configs_section: SectionProxy, config_hardlinks: boo
790
840
  # this is refreshed on every container start
791
841
 
792
842
  # always recreate the directory to pick up any changes
793
- if os.path.isdir(conf.configs_dir):
794
- shutil.rmtree(conf.configs_dir)
843
+ shutil.rmtree(conf.configs_dir, ignore_errors=True)
795
844
  os.makedirs(conf.configs_dir, mode=Consts.default_directory_mode(), exist_ok=True)
796
845
  if config_hardlinks:
797
846
  print_info("Creating hard links to paths specified in [configs] ...")
@@ -812,10 +861,7 @@ def process_configs_section(configs_section: SectionProxy, config_hardlinks: boo
812
861
  dest_path = f"{conf.configs_dir}/{dest_rel_path}"
813
862
  if os.access(src_path, os.R_OK):
814
863
  if os.path.exists(dest_path):
815
- if os.path.isdir(dest_path):
816
- shutil.rmtree(dest_path)
817
- else:
818
- os.unlink(dest_path)
864
+ shutil.rmtree(dest_path, ignore_errors=True)
819
865
  else:
820
866
  os.makedirs(os.path.dirname(dest_path),
821
867
  mode=Consts.default_directory_mode(), exist_ok=True)
@@ -842,7 +888,7 @@ def process_configs_section(configs_section: SectionProxy, config_hardlinks: boo
842
888
  print_warn(f"Skipping inaccessible configuration path '{src_path}'")
843
889
  print_info("DONE.")
844
890
  # finally mount the configs directory to corresponding directory in the target container
845
- add_mount_option(docker_args, conf.configs_dir, conf.target_configs_dir, "ro")
891
+ add_mount_option(docker_args, conf.configs_dir, conf.target_configs_dir)
846
892
 
847
893
 
848
894
  def process_env_section(env_section: SectionProxy, docker_args: list[str]) -> None:
@@ -927,13 +973,16 @@ def copytree(src_path: str, dest: str, hardlink: bool = False,
927
973
  Note: this function only handles regular files and directories (and hard/symbolic links to
928
974
  them) and will skip special files like device files, fifos etc.
929
975
 
930
- :param src_path: the resolved source directory (using `os.path.realpath` or `Path.resolve`)
931
- :param dest: the destination directory which should exist
976
+ :param src_path: the source directory to be copied (should have been resolved using
977
+ `os.path.realpath` or `Path.resolve` if `src_root` argument is not supplied)
978
+ :param dest: the destination directory which should not already exist (but its parent should)
932
979
  :param hardlink: if True then create hard links to the files in the source (so it should
933
980
  be in the same filesystem) else copy the files, defaults to False
934
- :param src_root: the resolved root source directory (same as `src_path` if `None`)
981
+ :param src_root: the resolved root source directory (same as `src_path` if `None` which is
982
+ assumed to have been resolved using `os.path.realpath` or `Path.resolve`)
935
983
  """
936
984
  src_root = src_root or src_path
985
+ src_root = src_root.rstrip("/")
937
986
  os.mkdir(dest, mode=stat.S_IMODE(os.stat(src_path).st_mode))
938
987
  # follow symlink if it leads to outside the "src" tree, else copy as a symlink which
939
988
  # ensures that all destination files are always accessible regardless of source going
@@ -951,7 +1000,7 @@ def copytree(src_path: str, dest: str, hardlink: bool = False,
951
1000
  os.symlink(l_name, dest_path)
952
1001
  continue
953
1002
  entry_path = os.path.realpath(entry.path)
954
- if entry_path.startswith(src_root) and entry_path[len(src_root)] == "/":
1003
+ if entry_path.startswith(src_root + "/"):
955
1004
  rpath = entry_path[len(src_root) + 1:]
956
1005
  os.symlink(("../" * rpath.count("/")) + rpath, dest_path)
957
1006
  continue
@@ -974,6 +1023,8 @@ def copytree(src_path: str, dest: str, hardlink: bool = False,
974
1023
  except OSError as err:
975
1024
  # ignore permission and related errors and continue
976
1025
  print_warn(f"Skipping copy/link of '{entry_path}' due to error: {err}")
1026
+ # TODO: SW: check for success in all copytree's else return False, then check at caller
1027
+ # to print a bold warning
977
1028
 
978
1029
 
979
1030
  def setup_ybox_scripts(conf: StaticConfiguration, distro_config: ConfigParser) -> None:
@@ -1138,8 +1189,8 @@ def run_container(docker_full_cmd: list[str], current_user: str, shared_root: st
1138
1189
  programs from less secure containers; the `ybox-pkg` tool provided a convenient high-level
1139
1190
  package manager that users should use for managing packages in the containers which will
1140
1191
  help in exposing packages only in designated containers
1141
- * systemd user service file can be generated for podman/docker to start the container
1142
- automatically on user login when -S/--systemd-service option has been provided
1192
+ * systemd user service file is generated for podman/docker to start the container
1193
+ automatically on user login (in absence of -S/--skip-systemd-service option)
1143
1194
 
1144
1195
  :param docker_full_cmd: the `docker`/`podman run -itd` command with all the options filled
1145
1196
  in from the container profile specification as a list of string
@@ -1207,26 +1258,28 @@ def create_and_start_service(box_name: str, env: Environ, systemctl: str, sys_pa
1207
1258
  svc_file = env.search_config_path("resources/ybox-systemd.template", only_sys_conf=True)
1208
1259
  with svc_file.open("r", encoding="utf-8") as svc_fd:
1209
1260
  svc_tmpl = svc_fd.read()
1210
- pid_file = ""
1211
1261
  if env.uses_podman:
1212
1262
  manager_name = "Podman"
1213
1263
  docker_requires = ""
1214
- res = run_command([env.docker_cmd, "container", "inspect", "--format={{.ConmonPidFile}}",
1215
- box_name], capture_output=True, exit_on_error=False)
1216
- if isinstance(res, str):
1217
- pid_file = f"PIDFile={res}"
1218
1264
  else:
1219
1265
  manager_name = "Docker"
1220
1266
  docker_requires = "After=docker.service\nRequires=docker.service\n"
1221
- systemd_dir = f"{env.home}/.config/systemd/user"
1222
- ybox_svc = f"ybox-{box_name}.service"
1223
- ybox_env = f"{systemd_dir}/.ybox-{box_name}.env"
1267
+ systemd_dir = env.systemd_user_conf_dir()
1268
+ ybox_svc_prefix = ybox_systemd_service_prefix(box_name)
1269
+ ybox_svc = f"{ybox_svc_prefix}.service"
1270
+ ybox_env = f".{ybox_svc_prefix}.env"
1224
1271
  formatted_now = env.now.astimezone().strftime("%a %d %b %Y %H:%M:%S %Z")
1272
+ # get the path of ybox-control and replace $HOME by %h to keep it generic
1273
+ if ybox_ctrl_path := shutil.which("ybox-control"):
1274
+ ybox_bin_dir = os.path.dirname(ybox_ctrl_path)
1275
+ if ybox_bin_dir.startswith(env.home + "/"):
1276
+ ybox_bin_dir = f"%h{ybox_bin_dir[len(env.home):]}"
1277
+ else:
1278
+ ybox_bin_dir = "%h/.local/bin"
1225
1279
  svc_content = svc_tmpl.format(name=box_name, version=product_version, date=formatted_now,
1226
1280
  manager_name=manager_name, docker_requires=docker_requires,
1227
- env_file=ybox_env, pid_file=pid_file)
1281
+ sys_path=sys_path, ybox_bin_dir=ybox_bin_dir, env_file=ybox_env)
1228
1282
  env_content = f"""
1229
- PATH={sys_path}:{env.home}/.local/bin
1230
1283
  SLEEP_SECS={{sleep_secs}}
1231
1284
  # set the container manager to the one configured during ybox-create
1232
1285
  YBOX_CONTAINER_MANAGER={env.docker_cmd}
@@ -1235,15 +1288,15 @@ def create_and_start_service(box_name: str, env: Environ, systemctl: str, sys_pa
1235
1288
  print_color(f"Generating user systemd service '{ybox_svc}' and reloading daemon", fgcolor.cyan)
1236
1289
  with open(f"{systemd_dir}/{ybox_svc}", "w", encoding="utf-8") as svc_fd:
1237
1290
  svc_fd.write(svc_content)
1238
- with open(ybox_env, "w", encoding="utf-8") as env_fd:
1291
+ with open(f"{systemd_dir}/{ybox_env}", "w", encoding="utf-8") as env_fd:
1239
1292
  env_fd.write(dedent(env_content.format(sleep_secs=0))) # don't sleep for the start below
1240
1293
  run_command([systemctl, "--user", "daemon-reload"], exit_on_error=False)
1241
1294
  run_command([systemctl, "--user", "enable", ybox_svc], exit_on_error=True)
1242
1295
  print_info(wait_msg)
1243
1296
  run_command([systemctl, "--user", "start", ybox_svc], exit_on_error=True)
1244
- # change SLEEP_SECS to 7 for subsequent starts
1245
- with open(ybox_env, "w", encoding="utf-8") as env_fd:
1246
- env_fd.write(dedent(env_content.format(sleep_secs=7)))
1297
+ # change SLEEP_SECS to 5 for subsequent starts
1298
+ with open(f"{systemd_dir}/{ybox_env}", "w", encoding="utf-8") as env_fd:
1299
+ env_fd.write(dedent(env_content.format(sleep_secs=5)))
1247
1300
 
1248
1301
 
1249
1302
  def start_container(docker_cmd: str, conf: StaticConfiguration) -> None:
ybox/run/destroy.py CHANGED
@@ -5,6 +5,7 @@ Code for the `ybox-destroy` script that is used to destroy an active or stopped
5
5
  import argparse
6
6
  import os
7
7
  import shutil
8
+ import subprocess
8
9
  import sys
9
10
 
10
11
  from ybox.cmd import check_ybox_exists, parser_version_check, run_command
@@ -36,15 +37,12 @@ def main_argv(argv: list[str]) -> None:
36
37
  check_ybox_exists(docker_cmd, container_name, exit_on_error=True)
37
38
  print_color(f"Stopping ybox container '{container_name}'", fg=fgcolor.cyan)
38
39
  # check if there is a systemd service for the container
39
- systemd_dir = f"{env.home}/.config/systemd/user"
40
- ybox_svc = f"ybox-{container_name}.service"
41
- ybox_svc_path = ""
42
- if (systemctl := shutil.which("systemctl", path=os.pathsep.join(Consts.sys_bin_dirs()))) and \
43
- not os.access(ybox_svc_path := f"{systemd_dir}/{ybox_svc}", os.R_OK):
44
- ybox_svc_path = ""
40
+ ybox_svc_prefix = ybox_systemd_service_prefix(container_name)
41
+ ybox_svc = f"{ybox_svc_prefix}.service"
42
+ systemctl = check_systemd_service_present(ybox_svc)
45
43
 
46
44
  # continue even if this fails since the container may already be in stopped state
47
- if systemctl and ybox_svc_path:
45
+ if systemctl:
48
46
  run_command([systemctl, "--user", "stop", ybox_svc],
49
47
  exit_on_error=False, error_msg=f"stopping '{container_name}'")
50
48
  else:
@@ -59,12 +57,16 @@ def main_argv(argv: list[str]) -> None:
59
57
  run_command(rm_args, error_msg=f"removing '{container_name}'")
60
58
 
61
59
  # remove systemd service file and reload daemon
62
- if systemctl and ybox_svc_path:
60
+ if systemctl:
63
61
  print_color(f"Removing systemd service '{ybox_svc}' and reloading daemon", fg=fgcolor.cyan)
64
62
  run_command([systemctl, "--user", "disable", ybox_svc], exit_on_error=False)
65
- os.unlink(ybox_svc_path)
63
+ systemd_dir = env.systemd_user_conf_dir()
64
+ try:
65
+ os.unlink(f"{systemd_dir}/{ybox_svc}")
66
+ except OSError:
67
+ pass
66
68
  try:
67
- os.unlink(f"{systemd_dir}/.ybox-{container_name}.env")
69
+ os.unlink(f"{systemd_dir}/.{ybox_svc_prefix}.env")
68
70
  except OSError:
69
71
  pass
70
72
  run_command([systemctl, "--user", "daemon-reload"], exit_on_error=False)
@@ -97,6 +99,26 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
97
99
  return parser.parse_args(argv)
98
100
 
99
101
 
102
+ def ybox_systemd_service_prefix(container_name: str) -> str:
103
+ """systemd service name prefix for given ybox container name"""
104
+ return container_name if container_name.startswith("ybox-") else f"ybox-{container_name}"
105
+
106
+
107
+ def check_systemd_service_present(user_svc: str) -> str:
108
+ """
109
+ Check if the given user systemd service is present and return the PATH of system installed
110
+ `systemctl` tool if true, else return empty string.
111
+
112
+ :param user_svc: name the user systemd service file
113
+ :return: full path of `systemctl` if installed and user systemd service is available else empty
114
+ """
115
+ if (systemctl := shutil.which("systemctl", path=os.pathsep.join(Consts.sys_bin_dirs()))) and \
116
+ subprocess.run([systemctl, "--user", "--quiet", "list-unit-files", user_svc],
117
+ check=False, capture_output=True).returncode == 0:
118
+ return systemctl
119
+ return ""
120
+
121
+
100
122
  def get_all_containers(docker_cmd: str) -> list[str]:
101
123
  """
102
124
  Get all the valid containers as known to the container manager.
ybox/run/graphics.py CHANGED
@@ -20,7 +20,8 @@ _STD_LIB_DIR_PATTERNS = ["&/usr/lib/*-linux-gnu", "&/lib/*-linux-gnu", "&/usr/li
20
20
  "&/lib64/*-linux-gnu", "&/usr/lib32/*-linux-gnu", "&/lib32/*-linux-gnu"]
21
21
  _STD_LD_LIB_PATH_VARS = ["LD_LIBRARY_PATH", "LD_LIBRARY_PATH_64", "LD_LIBRARY_PATH_32"]
22
22
  _NVIDIA_LIB_PATTERNS = ["*nvidia*.so*", "*NVIDIA*.so*", "libcuda*.so*", "libnvcuvid*.so*",
23
- "libnvoptix*.so*", "gbm/*nvidia*.so*", "vdpau/*nvidia*.so*"]
23
+ "libnvoptix*.so*", "gbm/*nvidia*.so*", "vdpau/*nvidia*.so*",
24
+ "libXNVCtrl.so*"]
24
25
  _NVIDIA_BIN_PATTERNS = ["nvidia-smi", "nvidia-cuda*", "nvidia-debug*", "nvidia-bug*"]
25
26
  # note that the code below assumes that file name pattern below is always of the form *nvidia*
26
27
  # (while others are directories), so if that changes then update _process_nvidia_data_files
@@ -45,7 +46,8 @@ def add_env_option(docker_args: list[str], env_var: str, env_val: Optional[str]
45
46
  docker_args.append(f"-e={env_var}={env_val}")
46
47
 
47
48
 
48
- def add_mount_option(docker_args: list[str], src: str, dest: str, flags: str = "") -> None:
49
+ def add_mount_option(docker_args: list[str], src: str, dest: str, flags: str = "",
50
+ check_exists: bool = False) -> None:
49
51
  """
50
52
  Add option to the list of podman/docker arguments to bind mount a source directory to
51
53
  given destination directory.
@@ -54,11 +56,40 @@ def add_mount_option(docker_args: list[str], src: str, dest: str, flags: str = "
54
56
  :param src: the source directory in the host system
55
57
  :param dest: the destination directory in the container
56
58
  :param flags: any additional flags to be passed to `-v` podman/docker argument, defaults to ""
59
+ :param check_exists: check if the bind mount was already added (and skip if so)
57
60
  """
58
- if flags:
59
- docker_args.append(f"-v={src}:{dest}:{flags}")
61
+ mount_arg = f"-v={src}:{dest}:{flags}" if flags else f"-v={src}:{dest}"
62
+ if not check_exists or mount_arg not in docker_args:
63
+ docker_args.append(mount_arg)
64
+
65
+
66
+ def handle_variable_mount(docker_args: list[str], env: Environ, mount_path: str) -> str:
67
+ """
68
+ Handle the case where a mount point may change in different starts or even within the same
69
+ started container instance. In these cases the "base" directory of the mount point is
70
+ mounted instead which should normally be `/tmp` or `$XDG_RUNTIME_DIR`. The variable values
71
+ are assumed to lie between these two, or the parent directory of the mount point if it does
72
+ not lie within these two base directories. The actual passing of the required environment
73
+ variable (that can change) is handled by the `run-in-dir` script that will adjust the variable
74
+ value to reflect that mount point added by this method.
75
+
76
+ :param docker_args: list of podman/docker arguments to which the options have to be appended
77
+ :param env: an instance of the current :class:`Environ`
78
+ :param mount_path: the variable path which is usually the value of an environment variable
79
+ :return: the result mount point inside the container for the `mount_path`
80
+ """
81
+ base_dir = os.path.dirname(mount_path)
82
+ # check if parent_dir is in $XDG_RUNTIME_DIR or /tmp
83
+ if not env.xdg_rt_dir:
84
+ base_dirs = {base_dir, "/tmp"}
85
+ elif mount_path.startswith(env.xdg_rt_dir + "/") or mount_path.startswith("/tmp/"):
86
+ base_dirs = (env.xdg_rt_dir, "/tmp")
87
+ base_dir = "/tmp" if base_dir.startswith("/tmp") else env.xdg_rt_dir
60
88
  else:
61
- docker_args.append(f"-v={src}:{dest}")
89
+ base_dirs = (base_dir, env.xdg_rt_dir, "/tmp")
90
+ for b_dir in base_dirs:
91
+ add_mount_option(docker_args, b_dir, f"{b_dir}-host", "ro", check_exists=True)
92
+ return mount_path.replace(base_dir, f"{base_dir}-host")
62
93
 
63
94
 
64
95
  def enable_x11(docker_args: list[str], env: Environ) -> None:
@@ -82,18 +113,7 @@ def enable_x11(docker_args: list[str], env: Environ) -> None:
82
113
  # parent can cause trouble if one changes the display manager, for example, which
83
114
  # uses an entirely different mount point (e.g. gdm uses /run/user/... while sddm
84
115
  # uses /tmp)
85
- parent_dir = os.path.dirname(xauth)
86
- # check if parent_dir is in $XDG_RUNTIME_DIR or /tmp
87
- if not env.xdg_rt_dir:
88
- parent_dirs = {parent_dir, "/tmp"}
89
- elif xauth.startswith(f"{env.xdg_rt_dir}/") or xauth.startswith("/tmp/"):
90
- parent_dirs = (env.xdg_rt_dir, "/tmp")
91
- parent_dir = "/tmp" if parent_dir.startswith("/tmp") else env.xdg_rt_dir
92
- else:
93
- parent_dirs = (parent_dir, env.xdg_rt_dir, "/tmp")
94
- for p_dir in parent_dirs:
95
- add_mount_option(docker_args, p_dir, f"{p_dir}-host", "ro")
96
- target_xauth = xauth.replace(parent_dir, f"{parent_dir}-host")
116
+ target_xauth = handle_variable_mount(docker_args, env, xauth)
97
117
  add_env_option(docker_args, "XAUTHORITY", target_xauth)
98
118
  add_env_option(docker_args, "XAUTHORITY_ORIG", target_xauth)
99
119
 
ybox/run/pkg.py CHANGED
@@ -392,11 +392,11 @@ def add_mark(subparser: argparse.ArgumentParser) -> None:
392
392
  subparser.add_argument("-e", "--explicit", action="store_true",
393
393
  help="mark the package as explicitly installed; the package will "
394
394
  "henceforth be managed by `ybox-pkg` if not already; "
395
- "exactly one of -e or -D option must be specified")
396
- subparser.add_argument("-D", "--dependency-of", type=str,
395
+ "exactly one of -e or -d option must be specified")
396
+ subparser.add_argument("-d", "--dependency-of", type=str,
397
397
  help="mark the package as a dependency of given package; both the "
398
398
  "packages will henceforth be managed by `ybox-pkg` if not "
399
- "already; exactly one of -e or -D option must be specified")
399
+ "already; exactly one of -e or -d option must be specified")
400
400
  subparser.add_argument("package", type=str, help="the package to be marked")
401
401
  subparser.set_defaults(func=mark_package)
402
402
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: ybox
3
- Version: 0.9.10
3
+ Version: 0.9.11
4
4
  Summary: Securely run Linux distribution inside a container
5
5
  Author-email: Sumedh Wale <sumwale@yahoo.com>, Vishal Rao <vishalrao@gmail.com>
6
6
  License: Copyright (c) 2024-2025 Sumedh Wale and contributors
@@ -25,7 +25,7 @@ License: Copyright (c) 2024-2025 Sumedh Wale and contributors
25
25
 
26
26
  Project-URL: Homepage, https://github.com/sumwale/ybox
27
27
  Project-URL: Issues, https://github.com/sumwale/ybox/issues
28
- Keywords: Linux in container,toolbox
28
+ Keywords: Linux in container,toolbox,distrobox
29
29
  Classifier: Development Status :: 4 - Beta
30
30
  Classifier: Intended Audience :: End Users/Desktop
31
31
  Classifier: License :: OSI Approved :: MIT License
@@ -42,6 +42,7 @@ License-File: LICENSE
42
42
  Requires-Dist: packaging
43
43
  Requires-Dist: simple-term-menu
44
44
  Requires-Dist: tabulate>=0.9.0
45
+ Dynamic: license-file
45
46
 
46
47
  ## Introduction
47
48
 
@@ -99,7 +100,7 @@ So, for example, if you want to run the latest and greatest Intellij IDEA commun
99
100
  to do is:
100
101
 
101
102
  ```sh
102
- # create an Arch Linux based container
103
+ # create an Arch Linux based container and generate systemd service file (if possible)
103
104
  ybox-create arch
104
105
  # then select an appropriate built-in profile e.g. "dev.ini" from the menu
105
106
 
@@ -197,6 +198,8 @@ to point to the full path of the podman or docker executable.
197
198
  ybox-create
198
199
  ```
199
200
 
201
+ By default this will also generate a user systemd service if possible (add `-S` or
202
+ `--skip-systemd-service` option to skip creation of a user systemd service).
200
203
  This will allow choosing from supported distributions, then from the available profiles.
201
204
  You can start with the Arch Linux distribution and `apps.ini` profile to try it out. The container
202
205
  will have a name like `ybox-<distribution>_<profile>` by default like `ybox-arch_apps` for the
@@ -269,7 +272,7 @@ ybox-pkg list -o
269
272
  ```
270
273
  To show more details of the packages (combine with -a/-o as required):
271
274
  ```sh
272
- ybox-pkg list -o
275
+ ybox-pkg list -v
273
276
  ```
274
277
 
275
278
  List all the files installed by the package:
@@ -309,6 +312,8 @@ Clean package cache, temporary downloads etc:
309
312
  ```sh
310
313
  ybox-pkg clean
311
314
  ```
315
+ Add `-q` option to answer yes for any questions automatically if all your containers use
316
+ the same shared root.
312
317
 
313
318
  Mark a package as explicitly installed (also registers with `ybox-pkg` if not present):
314
319
  ```sh
@@ -317,7 +322,7 @@ ybox-pkg mark firefox -e
317
322
 
318
323
  Mark a package as a dependency of another (also registers with `ybox-pkg` if not present):
319
324
  ```sh
320
- ybox-pkg mark qt5ct -D zoom # mark qt5ct as an optional dependency of zoom
325
+ ybox-pkg mark qt5ct -d zoom # mark qt5ct as an optional dependency of zoom
321
326
  ```
322
327
 
323
328
  Repair package installation after a failure or interrupt:
@@ -437,26 +442,32 @@ for a ybox container. See the full set of options with `ybox-control -h/--help`.
437
442
  ### Auto-starting containers
438
443
 
439
444
  Containers can be auto-started as per the usual way for rootless podman/docker services.
440
- This is triggered by systemd on user login which is exactly what we want for ybox
445
+ This is triggered by systemd on user login which is exactly what is required for ybox
441
446
  containers so that the container applications are available on login and are stopped on
442
- session logout. For docker the following should suffice:
447
+ session logout. All the tested Linux distributions support this and provide for user
448
+ systemd daemon on user login.
443
449
 
444
- ```sh
445
- systemctl --user enable docker
446
- ```
450
+ The `ybox-create` command autogenerates the systemd service file (in absence of `-S` or
451
+ `--skip-systemd-service` option) which is also removed by `ybox-destroy` automatically.
452
+ The name of the generated service is `ybox-<NAME>` where `<NAME>` is the name of the
453
+ container if `<NAME>` does not start with `ybox-` prefix, else it is just `<NAME>`.
447
454
 
448
- See [docker docs](https://docs.docker.com/engine/security/rootless/#daemon) for details.
449
-
450
- For podman you will need to explicitly generate systemd service file for each container and
451
- copy to your systemd configuration directory since podman does not use a background daemon.
452
- For the `ybox-arch_apps` container in the examples before:
455
+ With a user service installed, the `systemctl` commands can be used to control the
456
+ ybox container (`<SERVICE_NAME>` is `ybox-<NAME>/<NAME>` mentioned above):
453
457
 
454
458
  ```sh
455
- mkdir -p ~/.config/systemd/user/
456
- podman generate systemd --name ybox-arch_apps > ~/.config/systemd/user/container-ybox-arch_apps.service
457
- systemctl --user enable container-ybox-arch_apps.service
459
+ systemctl --user status <SERVICE_NAME> # show status of the service
460
+ systemctl --user stop <SERVICE_NAME> # stop the service
461
+ systemctl --user start <SERVICE_NAME> # start the service
458
462
  ```
459
463
 
464
+ If your Linux distribution does not use systemd, then the autostart has to be handled
465
+ manually as per the distribution's preferred way. For instance an appropriate desktop
466
+ file can be added to `~/.config/autostart` directory to start a ybox container on
467
+ graphical login, though performing a clean stop can be hard with this approach.
468
+ Note that the preferred way to start/stop a ybox container is using the `ybox-control`
469
+ command rather than directly using podman/docker.
470
+
460
471
 
461
472
  ## Development
462
473
 
@@ -1,7 +1,7 @@
1
- ybox/__init__.py,sha256=jQDT4EtS19JjBbrvOVG5G0Vzpcw9ZsATD9wt6wNRr1s,97
1
+ ybox/__init__.py,sha256=DJW4aoiXY2arxFg1eBQPfjHm19Ur_tXdLu332z66Esw,97
2
2
  ybox/cmd.py,sha256=RaNZ7LBqUNwpqQkitR29WLoItjkMfZmaFEeryLTR_tM,14962
3
3
  ybox/config.py,sha256=inmuUhlAZb6EKLGYWdymsqoHggtiLd58kc125l25ACA,9943
4
- ybox/env.py,sha256=vfHuvTOpApR4fLx1vePWRrTYxzo50c-7dFcnm1-vDHo,8738
4
+ ybox/env.py,sha256=6o0tJ163KWhVZHnhDRWw_16nMMYoy4-2yAHSJYBSL-0,9420
5
5
  ybox/filelock.py,sha256=nWBp3jvbtrNziRzNcWm6FVVA_lhMccLwgLCVT2IDK5c,3185
6
6
  ybox/print.py,sha256=hAQjTb6JmtjWh0sF4GdZHcKRph5iMKP5x23s8LE85q4,4343
7
7
  ybox/state.py,sha256=QSAfa4LGy7oDZVe6muBXFIylKB7CMalv3OgEQ3VtmAo,50174
@@ -27,27 +27,27 @@ ybox/conf/distros/deb-oldstable/distro.ini,sha256=lr7140ftndEvs3Liavf3hg3v0qHHWA
27
27
  ybox/conf/distros/deb-stable/distro.ini,sha256=xyQ31mLOpj3Z1MK-UYuc_9NfEkb7Gy10MdDKXMgnqDo,1100
28
28
  ybox/conf/distros/ubuntu2204/distro.ini,sha256=3Pw1Q30USyKMUcHp_cvlqhwyU0FPo8O2AHWkgd0cdE4,1105
29
29
  ybox/conf/distros/ubuntu2404/distro.ini,sha256=ra4_EteDsHrw9UcehP4qLGTn98gAHc4JCb56CDzz1Wo,1102
30
- ybox/conf/profiles/apps.ini,sha256=9GCZJvdB5yOfx-mXyti3M-YQjeLnugrhOp43sQQvqQ0,1001
31
- ybox/conf/profiles/basic.ini,sha256=mQuksPFcIoi1K-tsZxIpKhbNGe8Ltjquo0MacPoUUwE,18952
32
- ybox/conf/profiles/dev.ini,sha256=joSqLKrtokX4Tabn-EcxB4t7YhXlIV6ZnX3imCAPGaI,755
30
+ ybox/conf/profiles/apps.ini,sha256=vxVVy9oUw31to8CaagNZj9MQpGHIK2xARN4nzHy4vZ0,1270
31
+ ybox/conf/profiles/basic.ini,sha256=FGSDOooI4meXhQlwFpPo_RxZ62it3bvdjqWkpVVDcA4,19713
32
+ ybox/conf/profiles/dev.ini,sha256=Qicas_96ZoG3nsm1MSE1urarlBzIj9kBGmUqrk_cD3g,976
33
33
  ybox/conf/profiles/games.ini,sha256=FiBILNFOMpjKhrIR9nPbPj9QDUeI5h9wZaNXIb6Z7dc,1792
34
34
  ybox/conf/resources/entrypoint-base.sh,sha256=hcW8ZLHM-jlHx7McoKTo8HIFz_VBBPD0I3WqlX7LjHs,4890
35
35
  ybox/conf/resources/entrypoint-common.sh,sha256=fMopKBLeGuVV0ukANXh_ZHGTR1yGq108CTN_M2MNPyQ,461
36
- ybox/conf/resources/entrypoint-cp.sh,sha256=4R1UA2YLcKH_tUcyiioHSJA5B_t7GT1Z9gVFnCMi2a0,813
37
- ybox/conf/resources/entrypoint-root.sh,sha256=YhCftc4hYh3qKw0uEgYhHgitGY-KFGfHCxrHIMw9lHA,703
36
+ ybox/conf/resources/entrypoint-cp.sh,sha256=jemWFCqfme9blVtfVxqBzCsCp7b-bN2aFLajruF0GGQ,814
37
+ ybox/conf/resources/entrypoint-root.sh,sha256=58xcDX58ZtEFSr7ZSUFmzKt1OyGU29OrxjOmDiR6V8Q,706
38
38
  ybox/conf/resources/entrypoint-user.sh,sha256=Tps_UyQGyGn30LbgKm3gpzZtjOHnJpnlKx-px2GAd7A,698
39
- ybox/conf/resources/entrypoint.sh,sha256=jD4_C-2YtGwJ4pe2KfxfOtJll2Kv3F4YBU0WuhFQg9w,8716
39
+ ybox/conf/resources/entrypoint.sh,sha256=jbfFfUprU7ycp0moZLXNdnxk_bhtDxAk6byEiPMNz-s,8560
40
40
  ybox/conf/resources/prime-run,sha256=en8wEspc2Hzod9rq8KKnPQrjIPTLjUk44TqmtX-g0sw,339
41
- ybox/conf/resources/run-in-dir,sha256=WyYwCE56yuaMqoI1wGeil6UzycnlI6xxfy2iC92LDt4,1934
41
+ ybox/conf/resources/run-in-dir,sha256=-Xnwp4n6TK_2yPeBhGX6agenztyOtZ0316-vUlfOIag,2299
42
42
  ybox/conf/resources/run-user-bash-cmd,sha256=LMlPoHtzYNDcOI885ouBha9xGRnQ6AWCFVsSu2dxy10,1065
43
- ybox/conf/resources/ybox-systemd.template,sha256=-6Ui-J3GgW0VGoBdHlL7KCcGIrIgylXSKQvyzrKk7yE,647
44
- ybox/migrate/0.9.0-0.9.7:0.9.8.py,sha256=N7O7HtoMh_l404v2bHG5Od-_4fCDFMUBZ1SeT1P9F4M,1582
43
+ ybox/conf/resources/ybox-systemd.template,sha256=bA-bJOmc07Be-mQYtoFzl0yq4SJACv-FkAoKscPTh3g,779
44
+ ybox/migrate/0.9.0-0.9.10:0.9.11.py,sha256=WHvIWvUhXnruzSYwWYnv2zPNOlkXfAyaaJ_nZwG2wwE,1627
45
45
  ybox/pkg/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
46
  ybox/pkg/clean.py,sha256=UlsrJLfZOyKg-7BE8s9xPvAyuUXSwvU5x-E7MMjGJSE,1219
47
47
  ybox/pkg/info.py,sha256=HoEsiAi-iPEnAOWBMy9SsA4TOfyqHonRURU8k5EeAWA,1607
48
- ybox/pkg/inst.py,sha256=MxOfkY166En8vzyBN1FysXlpopxb4_0u5bAdmL7jgmw,36046
48
+ ybox/pkg/inst.py,sha256=wxFJo1GNrUGyjIqtEtOc_44Ty7zdZKj-qHcs9RdZudQ,36912
49
49
  ybox/pkg/list.py,sha256=Sk5THAmF132HKEZxJVDlqLLR8Z2w1wRA_E-gBsxlesQ,10097
50
- ybox/pkg/mark.py,sha256=cdTOKpo7Vb8j8lM0oZxdvAsJpIktnskyHWd6vEt-pG0,3748
50
+ ybox/pkg/mark.py,sha256=Hb1FaowdBx8x3GFcakXsq4ERNqI60Gjljr24qXsDGR8,3748
51
51
  ybox/pkg/repair.py,sha256=rCwXXJbilJYU73feIIVWGFdbkh6SDcquxgiEnYV6pNs,8080
52
52
  ybox/pkg/repo.py,sha256=25gQgGMPeHgX83iqqWXyG6V6rIZ4i_KEnIbNG-CrxTk,13677
53
53
  ybox/pkg/search.py,sha256=GKfonxCLHtH-kojZpRJlRfe0qf768wUehVv20nM4lKY,2526
@@ -55,13 +55,13 @@ ybox/pkg/uninst.py,sha256=ndqZ_WYr9HE5jVL1tQ_tTmSPyTFM6OJRCTLLmG-Qm4w,5338
55
55
  ybox/pkg/update.py,sha256=M-1MC8oh-baTHdSPWUUSTU_AhN3eu2nTCSge3bb6GmI,3269
56
56
  ybox/run/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
57
  ybox/run/cmd.py,sha256=a77RMC14Vovx2NV3LgdAzXVZXStC1TucgjcHPz4x03A,2105
58
- ybox/run/control.py,sha256=9-ETodWye6A6eUNGKYWrXtNGCHz0eC452N_ZQNXiIiI,7006
59
- ybox/run/create.py,sha256=DXEs1bXlC7tDK4PKNI2_O8PzRaDwuwmgN9Hy0CFDSV4,70109
60
- ybox/run/destroy.py,sha256=nbF0dvjTqqepFo2c3a0IlUxDuLrBtFqPqP-z96eRMH4,5467
61
- ybox/run/graphics.py,sha256=635Ry0sXIKCpM14ZPDHsQfw-xHNARbvaaAFNDpx3oo4,18991
58
+ ybox/run/control.py,sha256=Lvew2tqtWy5xcZMPXuh5sTrM2K92MykXfdMfBuKsIDc,7540
59
+ ybox/run/create.py,sha256=N2ULSoBsMWamBcm_JMLeZQdvMPhilhzFeM1aMkPDw7s,73062
60
+ ybox/run/destroy.py,sha256=i3N2zRCgrQM_lGA6W28O6Y-D1NIwBP64PA1e6vhOkL4,6315
61
+ ybox/run/graphics.py,sha256=mKQFU0la83Rv2V5l9TS25KIbqYmMnZjGQgGghLlfQp8,20309
62
62
  ybox/run/logs.py,sha256=pIdMWgNBNl-MgixArbMryUuBNNbi5JvDFP62IZ7jwr8,2050
63
63
  ybox/run/ls.py,sha256=7ylyxOOYEsVWK8baM0GaZcUlVQBwpdGiF7EhU09xf2s,2787
64
- ybox/run/pkg.py,sha256=dmv0iW-0KmcG400lRCcxQ1QCiiWJTda2kGJ1dRwyP_k,27219
64
+ ybox/run/pkg.py,sha256=6pes_wpVmPDiFYGfr8568GpyMByzor4dK5DSWaYmdsE,27219
65
65
  ybox/schema/0.9.1-added.sql,sha256=1rGp2DczZmmC_xwjmheeZNPSbDpFzasu6LO3tpTy3zI,1049
66
66
  ybox/schema/0.9.6-added.sql,sha256=Qcho6dP5OUpPUW3IBWl_kv88agMPHzueUAKqnZPnt3U,809
67
67
  ybox/schema/init.sql,sha256=fei8lPvjb-EIjm5zuA_XkEdjsIE3vtROhgRPt7QMlSs,1599
@@ -69,9 +69,9 @@ ybox/schema/migrate/0.9.0:0.9.1.sql,sha256=e9JGwrjFZXdWKGv2JQZlKcWz8DmOuUARpToSs
69
69
  ybox/schema/migrate/0.9.1:0.9.2.sql,sha256=X5J3unDS0eLeVvYKxQgx-iUBoAOk9T2suO34pWlQ-lE,362
70
70
  ybox/schema/migrate/0.9.2:0.9.3.sql,sha256=Y7GeBSuEEs7Hs9hh-KYDARgeeMgwQwercvTB5P_-k6I,102
71
71
  ybox/schema/migrate/0.9.5:0.9.6.sql,sha256=wqYhmputlUQzBI5zfP7O5kqIFWAbZQ05kolyHK4714A,70
72
- ybox-0.9.10.dist-info/LICENSE,sha256=7GbFgERMXSwD1VyLA5bo_XHvJipmNEUhDW5Sy51TFeY,1077
73
- ybox-0.9.10.dist-info/METADATA,sha256=oRc9oG6OSP28XaYhfRtp3sAXXqjkPKDEamSCtU-NHLk,24759
74
- ybox-0.9.10.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
75
- ybox-0.9.10.dist-info/entry_points.txt,sha256=xDlI_84Hl3ytYO_ERyt0rkJ4ioUF8Z1r49Hx1xL28Rk,243
76
- ybox-0.9.10.dist-info/top_level.txt,sha256=DYX7jvndHcBaJXLJ8vDyKrq0_KWoSeXXFq8r0d5L6Nk,5
77
- ybox-0.9.10.dist-info/RECORD,,
72
+ ybox-0.9.11.dist-info/licenses/LICENSE,sha256=7GbFgERMXSwD1VyLA5bo_XHvJipmNEUhDW5Sy51TFeY,1077
73
+ ybox-0.9.11.dist-info/METADATA,sha256=64TAyPzQLQVg4KBazcBTshFP8OPYS5D1npAgIvu9QDQ,25772
74
+ ybox-0.9.11.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
75
+ ybox-0.9.11.dist-info/entry_points.txt,sha256=xDlI_84Hl3ytYO_ERyt0rkJ4ioUF8Z1r49Hx1xL28Rk,243
76
+ ybox-0.9.11.dist-info/top_level.txt,sha256=DYX7jvndHcBaJXLJ8vDyKrq0_KWoSeXXFq8r0d5L6Nk,5
77
+ ybox-0.9.11.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.2)
2
+ Generator: setuptools (78.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5