shango/ivoire-forge/shango_forge.py

173 lines
7.0 KiB
Python

#!/usr/bin/env python3
"""
SHANGO FORGE — Moteur d'exécution multi-runtime Ivoire Monade
Fichiers: /root/shango/ivoire-forge/shango_forge.py
Runtime supporte: Go, Rust, Node, Python3, Web3, PyTorch, React
"""
import json, os, sys, subprocess, hashlib, shutil, time
from pathlib import Path
from typing import Dict, List, Optional
FORGE_ROOT = Path("/root/shango/ivoire-forge")
RUNTIMES = {
"go": {"cmd": "go", "ext": ".go", "docker": "golang:1.23-alpine"},
"rust": {"cmd": "rustc", "ext": ".rs", "docker": "rust:1.78-alpine"},
"node": {"cmd": "node", "ext": ".js", "docker": "node:22-alpine"},
"python3": {"cmd": "python3","ext": ".py", "docker": "python:3.12-slim"},
"web3": {"cmd": "node", "ext": ".sol", "docker": "node:22-alpine"},
"pytorch": {"cmd": "python3","ext": ".py", "docker": "pytorch/pytorch:2.3.1-cpu"},
"react": {"cmd": "node", "ext": ".tsx", "docker": "node:22-alpine"},
}
def log(msg: str): print(f"[forge] {msg}", flush=True)
def detect_runtime(source_dir: Path) -> Optional[str]:
"""Scanne le répertoire source et devine le runtime principal."""
files = list(source_dir.rglob("*"))
counts = {k: 0 for k in RUNTIMES}
for f in files:
if not f.is_file(): continue
for name, meta in RUNTIMES.items():
if f.name.endswith(meta["ext"]): counts[name] += 1
# Priorité: Go > Rust > React > Node > Python > Web3 > PyTorch
for candidate in ["go","rust","react","node","python3","web3","pytorch"]:
if counts.get(candidate, 0) > 0: return candidate
return None
def ensure_nix_shell(runtime: str) -> List[str]:
"""Retourne la commande pour entrer dans le devShell Nix approprié."""
shells = {
"go": "go",
"rust": "rust",
"node": "node",
"react": "node",
"python3": "python-ml",
"pytorch": "python-ml",
"web3": "web3",
}
shell = shells.get(runtime, "ivoire")
nix_sh = "/root/.nix-profile/etc/profile.d/nix.sh"
flake = "/root/ivoire-flakes"
return ["bash", "-c", f"source {nix_sh} && nix develop {flake}#{shell} -c bash -c"]
def build_docker(source_dir: Path, runtime: str, tag: str) -> str:
"""Construit une image Docker pour le projet source donné."""
meta = RUNTIMES.get(runtime, RUNTIMES["python3"])
dockerfile = source_dir / "Dockerfile"
if not dockerfile.exists():
# Génération auto Dockerfile minimal
dockerfile.write_text(f"""FROM {meta['docker']}
WORKDIR /app
COPY . .
{'RUN npm install' if runtime in ('node','react') else ''}
{'RUN go build -o app' if runtime == 'go' else ''}
{'RUN cargo build --release' if runtime == 'rust' else ''}
{'RUN pip install -r requirements.txt' if runtime in ('python3','pytorch') else ''}
CMD ["sh", "-c", "./app || python3 main.py || node index.js"]
""")
log(f"Docker build {tag} depuis {source_dir}")
subprocess.run(["docker", "build", "-t", tag, "."], cwd=source_dir, check=True)
return tag
def run_container(tag: str, name: str, ports: Dict[str,str] = None, env: Dict[str,str] = None, network: str = "infra") -> str:
"""Lance le container Docker sur le réseau infra."""
cmd = ["docker", "run", "-d", "--rm", "--name", name, "--network", network]
if ports:
for h, c in ports.items(): cmd += ["-p", f"{h}:{c}"]
if env:
for k, v in env.items(): cmd += ["-e", f"{k}={v}"]
cmd += [tag]
log(f"Container start: {' '.join(cmd)}")
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(result.stderr)
container_id = result.stdout.strip()[:12]
log(f"OK container={container_id}")
return container_id
def gitea_push(repo_dir: Path, repo_name: str, branch: str = "main"):
"""Pousse le code vers Gitea local."""
remote = f"http://admin:admin@git.ivoire-monade.shop:3002/admin/{repo_name}.git"
log(f"Gitea push {repo_name}")
subprocess.run(["git", "init", "-b", branch], cwd=repo_dir, capture_output=True)
subprocess.run(["git", "add", "."], cwd=repo_dir, capture_output=True)
subprocess.run(["git", "commit", "-m", "auto: shango forge init"], cwd=repo_dir, capture_output=True)
subprocess.run(["git", "remote", "add", "ivoire", remote], cwd=repo_dir, capture_output=True)
r = subprocess.run(["git", "push", "-u", "ivoire", branch], cwd=repo_dir, capture_output=True, text=True)
if "failed" in r.stderr.lower():
log(f"WARN push: {r.stderr.strip()}")
else:
log(f"OK push git.ivoire-monade.shop/{repo_name}")
# ── CLI ──────────────────────────────────────────────────────────────────
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Shango Forge — Multi-runtime builder")
sub = parser.add_subparsers(dest="cmd")
p_build = sub.add_parser("build", help="Build Docker image depuis source")
p_build.add_argument("source_dir")
p_build.add_argument("--tag", default="shango/forge:latest")
p_build.add_argument("--runtime", choices=list(RUNTIMES.keys()))
p_run = sub.add_parser("run", help="Run container")
p_run.add_argument("tag")
p_run.add_argument("--name", required=True)
p_run.add_argument("--port", action="append", default=[], help="host:container")
p_run.add_argument("--env", action="append", default=[], help="KEY=VAL")
p_detect = sub.add_parser("detect", help="Détecte runtime d'un dossier")
p_detect.add_argument("source_dir")
p_gitea = sub.add_parser("gitea-push", help="Push vers Gitea local")
p_gitea.add_argument("source_dir")
p_gitea.add_argument("--repo", required=True)
p_status = sub.add_parser("status", help="État des runtimes")
args = parser.parse_args()
if args.cmd == "detect":
src = Path(args.source_dir)
rt = detect_runtime(src)
print(json.dumps({"runtime": rt, "source": str(src)}, indent=2))
elif args.cmd == "build":
src = Path(args.source_dir)
rt = args.runtime or detect_runtime(src)
if not rt:
log("Runtime non détecté. Utilise --runtime.")
sys.exit(1)
tag = build_docker(src, rt, args.tag)
print(json.dumps({"image": tag, "runtime": rt}))
elif args.cmd == "run":
ports = {}
for p in args.port:
h, c = p.split(":")
ports[h] = c
envs = {}
for e in args.env:
k, v = e.split("=", 1)
envs[k] = v
cid = run_container(args.tag, args.name, ports, envs)
print(json.dumps({"container_id": cid, "name": args.name}))
elif args.cmd == "gitea-push":
gitea_push(Path(args.source_dir), args.repo)
elif args.cmd == "status":
for name, meta in RUNTIMES.items():
ok = shutil.which(meta["cmd"]) is not None
print(f" {name:12} {'OK' if ok else 'ABSENT'} ({meta['docker']})")
else:
parser.print_help()