173 lines
7.0 KiB
Python
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()
|