v3, heavy refactor needed

This commit is contained in:
2026-05-23 23:24:56 +03:00
parent 859f26de7b
commit 8fe04e77a2
22 changed files with 847 additions and 296 deletions
Executable
+49
View File
@@ -0,0 +1,49 @@
#!/bin/sh
set -e
IMAGE="${DNC_IMAGE:-ghcr.io/lstmnemodel/dnc:latest}"
GPU=none
if command -v nvidia-smi >/dev/null 2>&1; then
GPU=nvidia
fi
DRI=""
[ -d /dev/dri ] && DRI=1
KFD=""
[ -e /dev/kfd ] && KFD=1
DOCKER_SOCK="/var/run/docker.sock"
ROOTLESS=$(docker info --format '{{.ClientInfo.Context}}' 2>/dev/null | grep -ci rootless || true)
if [ "$ROOTLESS" -gt 0 ] 2>/dev/null ; then
echo "ERROR: dnc requires rootful Docker. Rootless Docker is not supported." >&2
exit 1
fi
DNC_CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/dnc"
mkdir -p "$DNC_CACHE"
TTY_FLAG=""
[ -t 0 ] && TTY_FLAG="-t"
exec docker run --rm -i ${TTY_FLAG} \
-v "$DOCKER_SOCK:/var/run/docker.sock:ro" \
-v "$PWD:$PWD" -w "$PWD" \
-v "$DNC_CACHE:/opt/dnc/host:rw" \
-e "DNC_HOST_GPU=$GPU" \
-e "DNC_HOST_DRI=$DRI" \
-e "DNC_HOST_KFD=$KFD" \
-e "DNC_HOST_UID=$(id -u)" \
-e "DNC_HOST_GID=$(id -g)" \
-e "DNC_HOST_USER=$USER" \
-e "DNC_HOST_HOME=$HOME" \
-e "DNC_HOST_SHELL=$SHELL" \
-e "DNC_HOST_GIDS=$(id -G)" \
-e "DNC_CACHE_HOST=$DNC_CACHE" \
-e DISPLAY -e WAYLAND_DISPLAY -e XAUTHORITY \
-e DBUS_SESSION_BUS_ADDRESS -e XDG_RUNTIME_DIR \
-e PULSE_SERVER -e PIPEWIRE_RUNTIME_DIR \
-e SSH_AUTH_SOCK -e TERM -e LANG \
"$IMAGE" "$@"
-16
View File
@@ -1,16 +0,0 @@
ARG BASE_IMAGE_DIGEST
FROM archlinux@${BASE_IMAGE_DIGEST}
ARG ALA_DATE
COPY pacman.conf /etc/pacman.conf
COPY packages.list /tmp/packages.list
RUN sed "s|\$ala_date|$ALA_DATE|g" -i /etc/pacman.conf && \
pacman -Syu --noconfirm && \
pacman -S --noconfirm $(grep -v '^#' /tmp/packages.list | grep -v '^$') && \
rm -f /tmp/packages.list
COPY config/ /usr/share/nnm/config/
RUN git clone --depth=1 --single-branch https://github.com/ohmyzsh/ohmyzsh.git /usr/share/ohmyzsh
-46
View File
@@ -1,46 +0,0 @@
font_family JetBrainsMono Nerd Font
font_size 13.0
disable_ligatures never
cursor_shape beam
cursor_blink_interval 0.5
window_padding_width 6
hide_window_decorations no
background_opacity 0.97
background #001e26
foreground #9bc1c2
cursor #f34a00
selection_background #003747
color0 #002731
color8 #006388
color1 #d01b24
color9 #f4153b
color2 #6bbe6c
color10 #50ee84
color3 #a57705
color11 #b17e28
color4 #2075c7
color12 #178dc7
color5 #c61b6e
color13 #e14d8e
color6 #259185
color14 #00b29e
color7 #e9e2cb
color15 #fcf4dc
selection_foreground #001e26
mouse_hide_wait -3.0
map ctrl+grave send_text all \x1b[96;5u
tab_bar_style powerline
tab_powerline_style slanted
map ctrl+shift+t new_tab_with_cwd
enabled_layouts splits,stack
map ctrl+shift+\ launch --location=vsplit --cwd=current
map ctrl+shift+- launch --location=hsplit --cwd=current
scrollback_pager nvim --noplugin -R -c "set nonumber norelativenumber" -
map ctrl+shift+enter launch --cwd=current --type=window nvim
-38
View File
@@ -1,38 +0,0 @@
vim.g.mapleader = " "
vim.g.maplocalleader = " "
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
vim.fn.system({
"git",
"clone",
"--filter=blob:none",
"https://github.com/folke/lazy.nvim.git",
"--branch=stable",
lazypath,
})
end
vim.opt.rtp:prepend(lazypath)
require("lazy").setup({
spec = {
{ "LazyVim/LazyVim", import = "lazyvim.plugins" },
{ import = "plugins" },
},
defaults = {
lazy = false,
version = false,
},
install = { colorscheme = { "tokyonight" } },
checker = { enabled = true },
performance = {
cache = { enabled = true },
rtp = {
disabled_plugins = {
"gzip", "matchit", "matchparen",
"netrwPlugin", "tarPlugin", "tohtml",
"tutor", "zipPlugin",
},
},
},
})
@@ -1,60 +0,0 @@
return {
{
"LazyVim/LazyVim",
opts = {
colorscheme = "tokyonight",
},
},
{
"folke/tokyonight.nvim",
opts = {
style = "night",
on_highlights = function(hl, c)
hl.Normal = { bg = "#001e26", fg = "#9bc1c2" }
hl.NormalFloat = { bg = "#002731" }
hl.LineNr = { fg = "#006388" }
hl.CursorLine = { bg = "#002731" }
hl.CursorLineNr = { fg = "#9bc1c2" }
hl.Visual = { bg = "#003747" }
hl.VisualNOS = { bg = "#003747" }
hl.Search = { bg = "#a57705", fg = "#001e26" }
hl.IncSearch = { bg = "#f34a00", fg = "#001e26" }
hl.Comment = { fg = "#006388", italic = true }
hl.String = { fg = "#6bbe6c" }
hl.Function = { fg = "#2075c7" }
hl.Number = { fg = "#c61b6e" }
hl.Boolean = { fg = "#c61b6e" }
hl.Type = { fg = "#259185" }
hl.Keyword = { fg = "#d01b24" }
hl.Identifier = { fg = "#9bc1c2" }
hl.Constant = { fg = "#a57705" }
hl.Special = { fg = "#e14d8e" }
hl.Title = { fg = "#178dc7", bold = true }
hl.Todo = { bg = "#a57705", fg = "#001e26" }
hl.Error = { fg = "#f4153b" }
hl.WarningMsg = { fg = "#b17e28" }
hl.MoreMsg = { fg = "#50ee84" }
hl.Question = { fg = "#178dc7" }
hl.Pmenu = { bg = "#002731", fg = "#9bc1c2" }
hl.PmenuSel = { bg = "#003747", fg = "#e9e2cb" }
hl.PmenuSbar = { bg = "#002731" }
hl.PmenuThumb = { bg = "#006388" }
hl.StatusLine = { bg = "#003747", fg = "#9bc1c2" }
hl.StatusLineNC = { bg = "#002731", fg = "#006388" }
hl.TabLine = { bg = "#002731", fg = "#006388" }
hl.TabLineSel = { bg = "#003747", fg = "#e9e2cb" }
hl.TabLineFill = { bg = "#001e26" }
hl.NonText = { fg = "#006388" }
hl.SpecialKey = { fg = "#00b29e" }
hl.MatchParen = { bg = "#003747", fg = "#f34a00" }
hl.DiffAdd = { bg = "#1a3a1a", fg = "#50ee84" }
hl.DiffChange = { bg = "#1a2a3a", fg = "#178dc7" }
hl.DiffDelete = { bg = "#3a1a1a", fg = "#f4153b" }
hl.DiffText = { bg = "#1a3a3a", fg = "#00b29e" }
hl.SpellBad = { undercurl = true, sp = "#f4153b" }
hl.SpellCap = { undercurl = true, sp = "#b17e28" }
hl.Whitespace = { fg = "#004455" }
end,
},
},
}
-8
View File
@@ -1,8 +0,0 @@
return {
{
"mbbill/undotree",
keys = {
{ "<leader>u", vim.cmd.UndotreeToggle, desc = "Undotree" },
},
},
}
-7
View File
@@ -1,7 +0,0 @@
ZSH=/usr/share/ohmyzsh
ZSH_THEME=agnoster
source $ZSH/oh-my-zsh.sh
export TERM=xterm-256color
prompt_context() { prompt_segment magenta black "%n" }
-46
View File
@@ -1,46 +0,0 @@
#!/bin/bash
set -euo pipefail
command -v podman >/dev/null 2>&1 || { echo "Error: podman not found"; exit 1; }
command -v distrobox >/dev/null 2>&1 || { echo "Error: distrobox not found"; exit 1; }
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CONTAINER="nnm"
ALA_DATE="$(grep '^ala_date' "$SCRIPT_DIR/manifest.toml" | sed 's/.*"\(.*\)"/\1/')"
BASE_DIGEST="$(grep '^base_image_digest' "$SCRIPT_DIR/manifest.toml" | sed 's/.*"\(.*\)"/\1/')"
TAG_INPUT="$(find "$SCRIPT_DIR/config" "$SCRIPT_DIR/Containerfile" -type f | sort | xargs -d'\n' sha256sum; cat "$SCRIPT_DIR/packages.list" "$SCRIPT_DIR/manifest.toml")"
IMAGE_TAG="nnm:$(echo -n "$TAG_INPUT" | sha256sum | cut -c1-16)"
IMAGE="localhost/$IMAGE_TAG"
if ! podman image exists "$IMAGE"; then
echo "==> Building image..."
podman build \
--build-arg "ALA_DATE=$ALA_DATE" \
--build-arg "BASE_IMAGE_DIGEST=$BASE_DIGEST" \
-t "$IMAGE" -f "$SCRIPT_DIR/Containerfile" "$SCRIPT_DIR"
fi
echo "==> Copying configs to ~/.config/nvimnemodel/..."
mkdir -p ~/.config/nvimnemodel
cp -r "$SCRIPT_DIR/config/"* ~/.config/nvimnemodel/
if ! distrobox list 2>/dev/null | grep -qw "$CONTAINER"; then
echo "==> Creating distrobox container..."
NVIDIA_FLAG=""
[[ -d /dev/nvidia0 ]] && NVIDIA_FLAG="--nvidia"
distrobox create --name "$CONTAINER" --image "$IMAGE" $NVIDIA_FLAG
else
echo "==> Upgrading container..."
distrobox upgrade "$CONTAINER"
fi
echo "==> Installing /usr/local/bin/nnm..."
sudo cp "$SCRIPT_DIR/nnm" /usr/local/bin/nnm
sudo chmod +x /usr/local/bin/nnm
echo ""
echo "==> nnm installed"
echo " Run 'nnm' to open a container terminal"
echo " Run 'nnm init' to create .envrc for direnv"
-2
View File
@@ -1,2 +0,0 @@
ala_date = "2026/05/17"
base_image_digest = "sha256:445aaca11be510adf10b9ddcf30259d4ae4c8474530a8d5d71ca93199184ee70"
-37
View File
@@ -1,37 +0,0 @@
#!/bin/bash
set -euo pipefail
CONTAINER="nnm"
CONFIG_DIR="$HOME/.config/nvimnemodel"
case "${1:-}" in
init)
DIR="${2:-$PWD}"
ENVRC="$DIR/.envrc"
if [[ -f "$ENVRC" ]]; then
echo ".envrc already exists at $ENVRC"
else
cat > "$ENVRC" <<-ENVEOF
if [ -z "\${NNM_ACTIVE:-}" ]; then
export NNM_ACTIVE=1
distrobox-enter $CONTAINER
fi
ENVEOF
echo "Created $ENVRC — run 'direnv allow' to activate"
fi
;;
-)
shift
exec distrobox-enter "$CONTAINER" -- "$@"
;;
*)
distrobox-enter "$CONTAINER" -- \
env XDG_CONFIG_HOME="$CONFIG_DIR" \
ZDOTDIR="$CONFIG_DIR/zsh" \
kitty --class="nnm" \
--config "$CONFIG_DIR/kitty/kitty.conf" \
--directory "$PWD" \
zsh </dev/null 2>/tmp/nnm-kitty.log &
disown
;;
esac
-8
View File
@@ -1,8 +0,0 @@
kitty
neovim
zsh
git
base-devel
mesa
vulkan-intel
vulkan-radeon
-11
View File
@@ -1,11 +0,0 @@
[options]
HoldPkg = pacman glibc
Architecture = auto
SigLevel = Required DatabaseOptional
LocalFileSigLevel = Optional
[core]
Server = https://archive.archlinux.org/repos/$ala_date/$repo/os/$arch
[extra]
Server = https://archive.archlinux.org/repos/$ala_date/$repo/os/$arch
-17
View File
@@ -1,17 +0,0 @@
#!/bin/bash
set -euo pipefail
echo "This will remove the nnm container and /usr/local/bin/nnm."
read -rp "Are you sure? [y/N] " confirm
[[ "$confirm" =~ ^[yY] ]] || { echo "Aborted."; exit 0; }
echo "==> Removing container..."
distrobox rm --force nnm 2>/dev/null || true
echo "==> Removing image..."
podman image rm localhost/nnm:* 2>/dev/null || true
echo "==> Removing /usr/local/bin/nnm..."
sudo rm -f /usr/local/bin/nnm
echo "==> nnm uninstalled."
+15
View File
@@ -0,0 +1,15 @@
ARG PYTHON_IMAGE
FROM ${PYTHON_IMAGE}
RUN pip install --no-cache-dir docker
ARG DOCKER_VERSION
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \
&& curl -fsSL "https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz" \
| tar xz -C /usr/local/bin --strip-components=1 docker/docker \
&& apt-get purge -y curl ca-certificates && apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/*
COPY src/dnc /opt/dnc/lib/dnc
ENV PYTHONPATH=/opt/dnc/lib:$PYTHONPATH
ENTRYPOINT ["python3", "-m", "dnc"]
+26
View File
@@ -0,0 +1,26 @@
__version__ = "0.2.0"
DEFAULT_IMAGE = "ubuntu:latest"
DNC_FILE = ".dnc"
LABEL_CONTAINER = "dnc.container"
LABEL_PROJECT = "dnc.project"
LABEL_VERSION = "dnc.version"
LABEL_IMAGE = "dnc.image"
LABEL_GPU = "dnc.gpu"
HOST_CACHE = "/opt/dnc/host"
INIT_SRC = "/opt/dnc/lib/dnc/init.sh"
INIT_DST = f"{HOST_CACHE}/init.sh"
ENTRY_SRC = "/opt/dnc/lib/dnc/entry.sh"
ENTRY_DST = f"{HOST_CACHE}/entry.sh"
HOST_GPU = "DNC_HOST_GPU"
HOST_DRI = "DNC_HOST_DRI"
HOST_KFD = "DNC_HOST_KFD"
HOST_UID = "DNC_HOST_UID"
HOST_GID = "DNC_HOST_GID"
HOST_USER = "DNC_HOST_USER"
HOST_HOME = "DNC_HOST_HOME"
HOST_SHELL = "DNC_HOST_SHELL"
HOST_CACHE_HOST = "DNC_CACHE_HOST"
HOST_GIDS = "DNC_HOST_GIDS"
+3
View File
@@ -0,0 +1,3 @@
from dnc.cli import main
main()
+56
View File
@@ -0,0 +1,56 @@
import os
DNC_MD_CONTENT = """# dnc
This project uses dnc (dockernecontainer). When `.dnc` is present,
run ALL commands inside the container by prefixing them with `dnc -`:
dnc - nvim
dnc - npm install
dnc - python manage.py migrate
"""
OPENCODE_INSTRUCTIONS = """## dnc
Refer to DNC.md for container usage instructions.
"""
CURSOR_RULES = """description: dnc container
globs: *
---
Refer to DNC.md for container usage instructions.
"""
def create_dnc_md(project_dir: str):
path = os.path.join(project_dir, "DNC.md")
if os.path.exists(path):
print("DNC.md already exists (use --force to overwrite)")
return
with open(path, "w") as f:
f.write(DNC_MD_CONTENT)
print("Created DNC.md")
def create_opencode(project_dir: str):
opencode_dir = os.path.join(project_dir, ".opencode")
os.makedirs(opencode_dir, exist_ok=True)
path = os.path.join(opencode_dir, "instructions.md")
if os.path.exists(path):
print(".opencode/instructions.md already exists (use --force to overwrite)")
return
with open(path, "w") as f:
f.write(OPENCODE_INSTRUCTIONS)
print("Created .opencode/instructions.md")
def create_cursor(project_dir: str):
cursor_dir = os.path.join(project_dir, ".cursor", "rules")
os.makedirs(cursor_dir, exist_ok=True)
path = os.path.join(cursor_dir, "dnc.mdc")
if os.path.exists(path):
print(".cursor/rules/dnc.mdc already exists (use --force to overwrite)")
return
with open(path, "w") as f:
f.write(CURSOR_RULES)
print("Created .cursor/rules/dnc.mdc")
+184
View File
@@ -0,0 +1,184 @@
import argparse
import os
import sys
from dnc import __version__, DEFAULT_IMAGE, DNC_FILE
from dnc import agents as agents_mod
from dnc import container as ctr
def resolve_container_name():
current = os.getcwd()
while True:
path = os.path.join(current, DNC_FILE)
if os.path.exists(path):
name = _read_field(path, "container")
if name:
return current, name
print(f"Error: {DNC_FILE} at {current} is malformed")
sys.exit(1)
parent = os.path.dirname(current)
if parent == current:
return None, None
current = parent
def _read_field(path: str, field: str) -> str | None:
with open(path) as f:
for line in f:
line = line.strip()
if line.startswith(f"{field} = "):
return line.split("=", 1)[1].strip()
return None
def _write_dnc(dnc_dir: str, container_name: str, version: str,
image: str, gpu: str):
path = os.path.join(dnc_dir, DNC_FILE)
with open(path, "w") as f:
f.write(f"container = {container_name}\n")
f.write(f"version = {version}\n")
f.write(f"image = {image}\n")
f.write(f"gpu = {gpu}\n")
def _show_help():
print(f"""dnc — dockernecontainer v{__version__}
Usage:
dnc Enter container
dnc - <cmd> Run command in container
dnc setup [--image IMG] Create container for this project
dnc rm Remove container + .dnc
dnc list List all dnc containers
dnc info Show container details
dnc agent [--opencode] [--cursor] Create DNC.md + agent rules
dnc version Show version
dnc help Show this help""")
def main():
args = sys.argv[1:]
if not args:
dnc_dir, name = resolve_container_name()
if name is None:
print("No .dnc found. Run 'dnc setup' first.")
sys.exit(1)
ctr.enter(name)
return
cmd = args[0]
if cmd in ("--help", "-h", "help"):
_show_help()
return
if cmd == "version":
print(f"dnc v{__version__}")
return
if cmd in ("list", "ls"):
containers = ctr.list_containers()
if not containers:
print("No dnc containers found.")
return
print(f"{'NAME':20} {'PROJECT':30} {'IMAGE':25} {'GPU':10} {'STATUS':10}")
print("-" * 95)
for c in containers:
proj = c["project"]
if len(proj) > 30:
proj = proj[:28] + ".."
print(
f"{c['name']:20} {proj:30} "
f"{c['image'][:23]:25} {c['gpu']:10} {c['status']:10}"
)
return
if cmd == "setup":
parser = argparse.ArgumentParser(prog="dnc setup")
parser.add_argument("--image", "-i", default=None)
parsed, _ = parser.parse_known_args(args[1:])
project_dir = os.getcwd()
dnc_path = os.path.join(project_dir, DNC_FILE)
if os.path.exists(dnc_path):
print(f"{DNC_FILE} already exists. Run 'dnc rm' first.")
return
image = parsed.image or DEFAULT_IMAGE
name = ctr._default_container_name(project_dir)
gpu = ctr._gpu()
ctr._pull_image(image)
print(f"Creating container '{name}'...")
container = ctr.create(image, name, project_dir)
_write_dnc(project_dir, name, __version__, image, gpu)
print(f"Created {DNC_FILE} — container starting in background.")
return
if cmd == "info":
dnc_dir, name = resolve_container_name()
if name is None:
print("No .dnc found.")
sys.exit(1)
info = ctr.inspect(name)
print(f"Container: {info['name']}")
print(f"Status: {info['status']}")
print(f"Image: {info['image']}")
print(f"GPU: {info['gpu']}")
print(f"Project: {info['project']}")
print(f"Created: {info['created']}")
print("\nLabels:")
for k, v in info["labels"].items():
print(f" {k}: {v}")
print("\nMounts:")
for m in info["mounts"]:
src = m.get("source") or "(anonymous)"
print(f" {src}{m['destination']} ({m['mode']})")
print("\nEnvironment:")
for k, v in sorted(info["env"].items()):
if not k.startswith("DNC_"):
display = v[:80] + "..." if len(v) > 80 else v
print(f" {k}={display}")
return
if cmd == "agent":
parser = argparse.ArgumentParser(prog="dnc agent")
parser.add_argument("--opencode", action="store_true")
parser.add_argument("--cursor", action="store_true")
parsed, _ = parser.parse_known_args(args[1:])
agents_mod.create_dnc_md(os.getcwd())
if parsed.opencode:
agents_mod.create_opencode(os.getcwd())
if parsed.cursor:
agents_mod.create_cursor(os.getcwd())
return
if cmd == "rm":
dnc_dir, name = resolve_container_name()
if name is None:
print("No .dnc found.")
sys.exit(1)
ctr.remove(name)
os.remove(os.path.join(dnc_dir, DNC_FILE))
print(f"Removed container '{name}' and {DNC_FILE}.")
return
if cmd == "-":
dnc_dir, name = resolve_container_name()
if name is None:
print("No .dnc found. Run 'dnc setup' first.")
sys.exit(1)
rest = args[1:]
if not rest:
ctr.enter(name)
else:
interactive = sys.stdin.isatty()
ctr.exec_passthrough(name, rest, interactive=interactive)
return
_show_help()
sys.exit(1)
+442
View File
@@ -0,0 +1,442 @@
import hashlib
import os
import re
import shutil
import subprocess
import sys
import time
import docker
from docker.types import DeviceRequest
from dnc import (
__version__,
ENTRY_DST,
ENTRY_SRC,
HOST_CACHE,
HOST_CACHE_HOST,
HOST_DRI,
HOST_GID,
HOST_GIDS,
HOST_HOME,
HOST_KFD,
HOST_SHELL,
HOST_UID,
HOST_USER,
HOST_GPU,
INIT_DST,
INIT_SRC,
)
_FORWARD_ENV = {
"DISPLAY",
"WAYLAND_DISPLAY",
"XAUTHORITY",
"XDG_SESSION_TYPE",
"DBUS_SESSION_BUS_ADDRESS",
"XDG_RUNTIME_DIR",
"PULSE_SERVER",
"PIPEWIRE_RUNTIME_DIR",
"SSH_AUTH_SOCK",
"TERM",
"LANG",
"NVIDIA_VISIBLE_DEVICES",
"NVIDIA_DRIVER_CAPABILITIES",
}
_SKIP_ENV = {
"HOME",
"PATH",
"PWD",
"SHELL",
"container",
"CONTAINER_ID",
HOST_GPU,
HOST_DRI,
HOST_KFD,
HOST_UID,
HOST_GID,
HOST_GIDS,
HOST_USER,
HOST_HOME,
HOST_SHELL,
HOST_CACHE_HOST,
}
def _docker_client():
try:
return docker.from_env()
except docker.errors.DockerException as e:
print("Error: cannot connect to Docker daemon.", file=sys.stderr)
print(f" {e}", file=sys.stderr)
sys.exit(1)
def _host_cache_setup():
os.makedirs(HOST_CACHE, exist_ok=True)
shutil.copy2(INIT_SRC, INIT_DST)
os.chmod(INIT_DST, 0o755)
shutil.copy2(ENTRY_SRC, ENTRY_DST)
os.chmod(ENTRY_DST, 0o755)
def _default_container_name(project_dir: str) -> str:
safe = re.sub(r"[^a-zA-Z0-9_-]", "_", os.path.basename(project_dir))
h = hashlib.sha256(project_dir.encode()).hexdigest()[:8]
return f"dnc-{safe}-{h}"
def _gpu() -> str:
return os.environ.get(HOST_GPU, "none")
def _pull_image(image: str):
client = _docker_client()
try:
client.images.get(image)
return
except docker.errors.ImageNotFound:
pass
try:
print(f"Pulling {image}...")
client.images.pull(image)
except docker.errors.ImageNotFound:
print(f"Error: image '{image}' not found.", file=sys.stderr)
print(f" Check the name and try again.", file=sys.stderr)
sys.exit(1)
except docker.errors.APIError as e:
print(f"Error: failed to pull image '{image}': {e}", file=sys.stderr)
sys.exit(1)
def _require_container(client, name: str):
try:
return client.containers.get(name)
except docker.errors.NotFound:
print(f"Error: container '{name}' not found.", file=sys.stderr)
print(f" It may have been deleted outside dnc.", file=sys.stderr)
sys.exit(1)
def create(image: str, name: str, project_dir: str):
_host_cache_setup()
client = _docker_client()
gpu = _gpu()
uid = int(os.environ[HOST_UID])
gid = int(os.environ[HOST_GID])
host_home = os.environ[HOST_HOME]
shell = os.environ.get(HOST_SHELL, "/bin/bash")
host_cache = os.environ[HOST_CACHE_HOST]
volumes = {
host_home: {"bind": "/run/host/home", "mode": "ro"},
project_dir: {"bind": project_dir, "mode": "rw"},
"/tmp": {"bind": "/tmp", "mode": "rw"},
f"/run/user/{uid}": {"bind": f"/run/user/{uid}", "mode": "rw"},
"/tmp/.X11-unix": {"bind": "/tmp/.X11-unix", "mode": "rw"},
"/etc/hosts": {"bind": "/etc/hosts", "mode": "ro"},
"/etc/resolv.conf": {"bind": "/etc/resolv.conf", "mode": "ro"},
f"{host_cache}/init.sh": {"bind": "/usr/bin/dnc-init", "mode": "ro"},
f"{host_cache}/entry.sh": {"bind": "/usr/bin/dnc-entry", "mode": "ro"},
}
if os.path.exists("/etc/hostname"):
volumes["/etc/hostname"] = {"bind": "/etc/hostname", "mode": "ro"}
host_dri = os.environ.get(HOST_DRI, "")
host_kfd = os.environ.get(HOST_KFD, "")
device_requests = []
if gpu == "nvidia":
device_requests = [
DeviceRequest(count=-1, capabilities=[["gpu"]])
]
if host_dri:
volumes["/dev/dri"] = {"bind": "/dev/dri", "mode": "rw"}
if host_kfd:
volumes["/dev/kfd"] = {"bind": "/dev/kfd", "mode": "rw"}
env = {
"HOME": host_home,
"SHELL": os.path.basename(shell),
"container": "docker",
"CONTAINER_ID": name,
HOST_UID: uid,
HOST_GID: gid,
HOST_USER: os.environ[HOST_USER],
HOST_HOME: host_home,
HOST_SHELL: shell,
}
for key in _FORWARD_ENV:
val = os.environ.get(key)
if val:
env[key] = val
if gpu == "nvidia":
env["NVIDIA_VISIBLE_DEVICES"] = "all"
env["NVIDIA_DRIVER_CAPABILITIES"] = "all"
host_gids = os.environ.get(HOST_GIDS, "")
group_add = [f"{g}" for g in host_gids.split() if g] if host_gids else None
device_cgroup_rules = []
if host_dri:
device_cgroup_rules.append("c 226:* rwm")
if host_kfd:
device_cgroup_rules.append("c 253:* rwm")
labels = {
"dnc.container": "true",
"dnc.project": project_dir,
"dnc.version": __version__,
"dnc.image": image,
"dnc.gpu": gpu,
}
try:
container = client.containers.create(
image,
name=name,
entrypoint=["/usr/bin/dnc-init"],
command=["/bin/bash"],
stdin_open=True,
tty=True,
cap_add=[
"SYS_PTRACE",
"SYS_ADMIN",
"NET_RAW",
"NET_ADMIN",
"SYS_NICE",
"IPC_LOCK",
],
security_opt=["label=disable"],
pid_mode="host",
network_mode="host",
ipc_mode="host",
pids_limit=-1,
user="root:root",
group_add=group_add,
device_cgroup_rules=device_cgroup_rules or None,
environment=env,
volumes=volumes,
device_requests=device_requests,
labels=labels,
detach=True,
)
container.start()
except docker.errors.APIError as e:
print(f"Error: failed to create/start container '{name}'.", file=sys.stderr)
print(f" {e}", file=sys.stderr)
sys.exit(1)
return container
def _shell_needed(cmd: list[str]) -> bool:
metachars = set('|>&;<>$`\'"!#(){}[]*?~ \t')
return any(
any(c in metachars for c in arg)
for arg in cmd
)
def _ensure_executable(cmd: list[str]) -> list[str]:
if _shell_needed(cmd):
return ["sh", "-c", " ".join(cmd)]
return cmd
def _wait_init(container):
max_retries = 120
for _ in range(max_retries):
try:
exit_code, _ = container.exec_run(
"test -f /.dnc-setup-done", tty=False
)
if exit_code == 0:
return
except docker.errors.APIError:
pass
time.sleep(0.2)
def enter(name: str, cmd: list[str] | None = None):
client = _docker_client()
container = _require_container(client, name)
if container.status != "running":
try:
container.start()
container.reload()
while container.status != "running":
time.sleep(0.1)
container.reload()
except docker.errors.APIError as e:
print(f"Error: failed to start container '{name}'.", file=sys.stderr)
print(f" {e}", file=sys.stderr)
sys.exit(1)
_wait_init(container)
uid = os.environ[HOST_UID]
gid = os.environ[HOST_GID]
env_args = []
for key, val in os.environ.items():
if key in _SKIP_ENV:
continue
if '"' in val or "`" in val or "$" in val:
continue
env_args += ["-e", f"{key}={val}"]
fhs = [
"/usr/local/sbin", "/usr/local/bin", "/usr/sbin",
"/usr/bin", "/sbin", "/bin",
]
path = os.environ.get("PATH", "")
for p in fhs:
if p not in path:
path = f"{path}:{p}"
env_args += ["-e", f"PATH={path}"]
workdir = os.getcwd()
docker_args = [
"docker",
"exec",
"-i",
"--user", f"{uid}:{gid}",
]
if sys.stdin.isatty():
docker_args.append("-t")
docker_args += [
"--workdir", workdir,
*env_args,
"--detach-keys", "",
name,
]
if cmd:
docker_args.extend(_ensure_executable(cmd))
else:
docker_args.append("/usr/bin/dnc-entry")
try:
os.execvp("docker", docker_args)
except FileNotFoundError:
print("Error: 'docker' executable not found on PATH.", file=sys.stderr)
sys.exit(1)
def exec_passthrough(name: str, cmd: list[str], interactive: bool = False):
if interactive and sys.stdin.isatty():
enter(name, cmd=cmd)
return
client = _docker_client()
container = _require_container(client, name)
if container.status != "running":
try:
container.start()
container.reload()
while container.status != "running":
time.sleep(0.1)
container.reload()
except docker.errors.APIError as e:
print(f"Error: failed to start container '{name}'.", file=sys.stderr)
print(f" {e}", file=sys.stderr)
sys.exit(1)
_wait_init(container)
uid = os.environ[HOST_UID]
gid = os.environ[HOST_GID]
env = {}
for key, val in os.environ.items():
if key in _SKIP_ENV:
continue
if '"' in val or "`" in val or "$" in val:
continue
env[key] = val
exit_code, output = container.exec_run(
_ensure_executable(cmd),
user=f"{uid}:{gid}",
workdir=os.getcwd(),
environment=env,
tty=False,
stream=False,
stdin=True,
)
if output:
sys.stdout.buffer.write(output)
sys.stdout.buffer.flush()
sys.exit(exit_code)
def list_containers() -> list[dict]:
client = _docker_client()
results = []
for c in client.containers.list(
all=True, filters={"label": "dnc.container=true"}
):
lbl = c.labels
results.append({
"name": c.name,
"project": lbl.get("dnc.project", "?"),
"image": lbl.get("dnc.image", "?"),
"gpu": lbl.get("dnc.gpu", "?"),
"status": c.status,
"created": c.attrs.get("Created", ""),
})
results.sort(key=lambda x: x["created"], reverse=True)
return results
def remove(name: str):
client = _docker_client()
try:
c = client.containers.get(name)
except docker.errors.NotFound:
print(f"Warning: container '{name}' does not exist (already removed?).", file=sys.stderr)
return
c.remove(force=True, v=True)
def inspect(name: str) -> dict:
client = _docker_client()
c = _require_container(client, name)
raw_env = c.attrs.get("Config", {}).get("Env", [])
parsed_env = {}
for e in raw_env:
if "=" in e:
k, v = e.split("=", 1)
parsed_env[k] = v
mounts = [
{
"source": m.get("Source"),
"destination": m.get("Destination"),
"mode": m.get("Mode"),
}
for m in c.attrs.get("Mounts", [])
]
return {
"name": c.name,
"status": c.status,
"image": c.image.tags[0] if c.image.tags else "?",
"labels": dict(c.labels),
"created": c.attrs.get("Created"),
"env": parsed_env,
"mounts": mounts,
"gpu": c.labels.get("dnc.gpu", "none"),
"project": c.labels.get("dnc.project", "?"),
}
+14
View File
@@ -0,0 +1,14 @@
#!/bin/sh
if [ -d /run/host/home ]; then
for f in .gitconfig .ssh; do
src="/run/host/home/$f"
dst="$HOME/$f"
if [ -e "$src" ] && [ ! -e "$dst" ]; then
cp -r "$src" "$dst" 2>/dev/null || true
fi
done
fi
if [ -x /opt/dnc/entrypoint.sh ]; then
exec /opt/dnc/entrypoint.sh
fi
exec "${SHELL:-/bin/bash}" -l
+56
View File
@@ -0,0 +1,56 @@
#!/bin/bash
set -e
HOST_USER="${DNC_HOST_USER:-$(id -un)}"
HOST_UID="${DNC_HOST_UID:-1000}"
HOST_GID="${DNC_HOST_GID:-1000}"
HOST_HOME="${DNC_HOST_HOME:-/home/$HOST_USER}"
if ! getent group "$HOST_GID" >/dev/null 2>&1; then
groupadd -g "$HOST_GID" "$HOST_USER" 2>/dev/null || \
printf "%s:x:%s:\n" "$HOST_USER" "$HOST_GID" >> /etc/group
fi
EXISTING_USER=$(getent passwd "$HOST_UID" 2>/dev/null | cut -d: -f1)
if [ -n "$EXISTING_USER" ] && [ "$EXISTING_USER" != "$HOST_USER" ]; then
EXISTING_GROUP=$(getent group "$HOST_GID" 2>/dev/null | cut -d: -f1)
if [ -n "$EXISTING_GROUP" ] && [ "$EXISTING_GROUP" != "$HOST_USER" ]; then
groupmod -n "$HOST_USER" "$EXISTING_GROUP" 2>/dev/null || true
fi
usermod -l "$HOST_USER" -s /bin/bash -d "$HOST_HOME" \
-m "$EXISTING_USER" 2>/dev/null || true
elif [ -z "$EXISTING_USER" ]; then
useradd -M -u "$HOST_UID" -g "$HOST_GID" -s /bin/bash \
-d "$HOST_HOME" "$HOST_USER" 2>/dev/null || true
fi
mkdir -p "$HOST_HOME"
chown "$HOST_UID:$HOST_GID" "$HOST_HOME" 2>/dev/null || true
SUDO_USER="$HOST_USER"
if ! command -v sudo >/dev/null 2>&1; then
if command -v apt-get >/dev/null 2>&1; then
DEBIAN_FRONTEND=noninteractive apt-get update -qq && \
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq sudo || true
elif command -v dnf >/dev/null 2>&1; then
dnf install -y sudo || true
elif command -v yum >/dev/null 2>&1; then
yum install -y sudo || true
elif command -v apk >/dev/null 2>&1; then
apk add sudo || true
elif command -v zypper >/dev/null 2>&1; then
zypper install -y sudo || true
fi
fi
if command -v sudo >/dev/null 2>&1; then
mkdir -p /etc/sudoers.d
printf "%s ALL=(ALL) NOPASSWD:ALL\n" "$SUDO_USER" > /etc/sudoers.d/dnc-user
chmod 0440 /etc/sudoers.d/dnc-user
if sudo --version 2>&1 | head -1 | grep -qv sudo-rs; then
printf "Defaults !requiretty\n" >> /etc/sudoers
fi
fi
touch /.dnc-setup-done
exec "$@"
+2
View File
@@ -0,0 +1,2 @@
PYTHON_IMAGE="python:3.14-slim"
DOCKER_VERSION="29.5.2"