v3, heavy refactor needed
This commit is contained in:
@@ -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" "$@"
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
return {
|
|
||||||
{
|
|
||||||
"mbbill/undotree",
|
|
||||||
keys = {
|
|
||||||
{ "<leader>u", vim.cmd.UndotreeToggle, desc = "Undotree" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -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" }
|
|
||||||
@@ -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"
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
ala_date = "2026/05/17"
|
|
||||||
base_image_digest = "sha256:445aaca11be510adf10b9ddcf30259d4ae4c8474530a8d5d71ca93199184ee70"
|
|
||||||
@@ -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
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
kitty
|
|
||||||
neovim
|
|
||||||
zsh
|
|
||||||
git
|
|
||||||
base-devel
|
|
||||||
mesa
|
|
||||||
vulkan-intel
|
|
||||||
vulkan-radeon
|
|
||||||
@@ -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
|
|
||||||
@@ -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."
|
|
||||||
@@ -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"]
|
||||||
@@ -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"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from dnc.cli import main
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -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
@@ -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)
|
||||||
@@ -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", "?"),
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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 "$@"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
PYTHON_IMAGE="python:3.14-slim"
|
||||||
|
DOCKER_VERSION="29.5.2"
|
||||||
Reference in New Issue
Block a user