#!/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()