Skip to content

Instantly share code, notes, and snippets.

@ochafik
Last active October 14, 2025 02:08
Show Gist options
  • Select an option

  • Save ochafik/d4d49db7c9844eb85e9d161fdfafaf44 to your computer and use it in GitHub Desktop.

Select an option

Save ochafik/d4d49db7c9844eb85e9d161fdfafaf44 to your computer and use it in GitHub Desktop.
crun: to make, what npx is to npm and uvx to uv
uv tool install git+https://gist.github.com/ochafik/d4d49db7c9844eb85e9d161fdfafaf44

crun https://github.com/ggml-org/llama.cpp -- llama-server --fim-qwen-14b-spec
crun https://github.com/ggml-org/llama.cpp -- llama-tts --tts-oute-default -p 'Hello olivier how are we doing today?' && open output.wav
#!/usr/bin/env python3
"""
crun - cmake runner (npx for cmake projects)
Usage:
crun <repo-url>[@branch] [cmake-args] [-- bin [args]]
Examples:
crun https://github.com/ggml-org/llama.cpp -- llama-server --fim-qwen-14b-spec
crun https://github.com/ggml-org/llama.cpp -- llama-tts --tts-oute-default -p 'Hello olivier how are we doing today?' && open output.wav
"""
import sys, subprocess, tempfile, hashlib
from pathlib import Path
from urllib.parse import urlparse
from pydantic import BaseModel, Field, field_validator
class Config(BaseModel):
"""Build configuration"""
url: str
branch: str | None = None
cmake_args: list[str] = Field(default_factory=list)
build_type: str = "Release"
bin_name: str | None = None
bin_args: list[str] = Field(default_factory=list)
@field_validator("url")
@classmethod
def normalize_url(cls, v: str) -> str:
return v.removesuffix(".git")
@property
def repo_id(self) -> str:
"""Unique repo identifier for caching"""
p = urlparse(self.url)
return f"{p.netloc or 'local'}__{p.path.lstrip('/').replace('/', '__').replace('..', '_')}"
@property
def ref(self) -> str:
"""Git ref to checkout"""
return self.branch or "HEAD"
def build_cache_key(self, ref: str) -> str:
"""Unique build cache key: repo + ref + cmake args"""
args_str = " ".join(sorted(self.cmake_args))
combined = f"{self.repo_id}::{ref}::{args_str}"
hash_hex = hashlib.sha256(combined.encode()).hexdigest()[:16]
return hash_hex
def parse_args() -> Config:
"""Parse command-line arguments into Config"""
if len(sys.argv) < 2:
sys.exit("Usage: crun <repo-url>[@branch] [cmake-args] [-- bin [args]]")
url, *args = sys.argv[1:]
branch = url.split("@", 1)[1] if "@" in url else None
url = url.split("@")[0]
sep = args.index("--") if "--" in args else len(args)
cmake_args, bin_args = args[:sep], args[sep+1:]
# Extract CMAKE_BUILD_TYPE
build_type = "Release"
filtered_args = []
for arg in cmake_args:
if "CMAKE_BUILD_TYPE=" in arg:
build_type = arg.split("=")[1]
elif "CMAKE_BUILD_TYPE" not in arg:
filtered_args.append(arg)
return Config(
url=url,
branch=branch,
cmake_args=[*filtered_args, f"-DCMAKE_BUILD_TYPE={build_type}"],
build_type=build_type,
bin_name=bin_args[0] if bin_args else None,
bin_args=bin_args[1:] if len(bin_args) > 1 else []
)
def run(cmd: list[str], **kw) -> None:
"""Execute command, raising on failure"""
subprocess.run(cmd, check=True, **kw)
def setup_repo(cfg: Config, cache: Path) -> Path:
"""Clone or update repository, return repo directory"""
repo_dir = cache / "repos" / cfg.repo_id
repo_dir.parent.mkdir(parents=True, exist_ok=True)
if repo_dir.exists():
print(f"Fetching updates for {cfg.repo_id}")
run(["git", "-C", str(repo_dir), "fetch", "--depth", "1", "origin", cfg.ref])
else:
clone_args = ["git", "clone", "--depth", "1"]
if cfg.branch:
clone_args.extend(["--branch", cfg.branch, "--single-branch"])
clone_args.extend([cfg.url, str(repo_dir)])
print(f"Cloning {cfg.url}" + (f"@{cfg.branch}" if cfg.branch else ""))
run(clone_args)
return repo_dir
def create_worktree(cfg: Config, repo_dir: Path, cache: Path) -> tuple[Path, Path]:
"""Create git worktree and persistent build directory"""
# Reuse worktree if it exists for this ref
work_dir = cache / "worktrees" / f"{cfg.repo_id}.{cfg.ref.replace('/', '_')}"
if work_dir.exists():
print(f"Reusing worktree for {cfg.ref}")
else:
work_dir.parent.mkdir(parents=True, exist_ok=True)
print(f"Checking out {cfg.ref}")
try:
run(["git", "-C", str(repo_dir), "worktree", "add", str(work_dir), cfg.ref])
except subprocess.CalledProcessError:
print(f"Fetching missing ref: {cfg.ref}")
run(["git", "-C", str(repo_dir), "fetch", "--depth", "1", "origin", cfg.ref])
run(["git", "-C", str(repo_dir), "worktree", "add", str(work_dir), "FETCH_HEAD"])
# Persistent build directory based on repo + ref + cmake args
build_key = cfg.build_cache_key(cfg.ref)
build_dir = cache / "builds" / build_key
build_dir.mkdir(parents=True, exist_ok=True)
return work_dir, build_dir
def build_project(cfg: Config, work_dir: Path, build_dir: Path) -> Path:
"""Build with CMake, return build directory"""
print(f"Building ({cfg.build_type}) in {build_dir.name}")
run(["cmake", "-S", str(work_dir), "-B", str(build_dir), *cfg.cmake_args])
run(["cmake", "--build", str(build_dir), "--parallel", "--config", cfg.build_type, "-t", cfg.bin_name or "all"])
return build_dir
def run_binary(cfg: Config, build_dir: Path) -> None:
"""Execute binary if specified"""
if not cfg.bin_name:
return
exe = build_dir / cfg.bin_name
if not exe.exists():
exe = build_dir / cfg.build_type / cfg.bin_name # Try config subdir
if not exe.exists():
exe = build_dir / "bin" / cfg.bin_name # Try bin subdir
if not exe.exists():
sys.exit(f"Error: Binary '{cfg.bin_name}' not found in build directory")
print(f"Running {exe.name}")
subprocess.run([str(exe), *cfg.bin_args]) # Let exit code through
def main() -> None:
cfg = parse_args()
cache = Path.home() / ".cache" / "crun"
repo_dir = setup_repo(cfg, cache)
work_dir, build_dir = create_worktree(cfg, repo_dir, cache)
build_project(cfg, work_dir, build_dir)
run_binary(cfg, build_dir)
if __name__ == "__main__":
main()
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "crun"
version = "0.1.0"
description = "CMake runner - npx for cmake projects"
readme = "README.md"
requires-python = ">=3.11"
dependencies = ["pydantic>=2.0"]
[project.scripts]
crun = "crun:main"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment