# jj (jujutsu) — control de versiones moderno, compatible con git, en Rust puro # # ── POR QUÉ ENTRA ────────────────────────────────────────────────────────────────────────────── # 32.018★, Apache-2.0, 549 crates, un solo binario sin interfaz gráfica. Lo que lo hace barato para # esta casa sale del `Cargo.lock` y no del README: **habla git sin `libgit2`**. Son **53 crates # `gix-*`** (gitoxide, git reimplementado en Rust) y **cero `git2`/`libgit2-sys`** ⇒ ninguna receta C # nueva, ningún `.pc` que buscar. El único C del árbol es `libmimalloc-sys`, que trae su propio # fuente y lo compila con el crate `cc` — de ahí el envoltorio de `AR`. # # No reemplaza a `git`, que sigue siendo el que usa todo el repo y la granja: convive con él sobre # los mismos repositorios. # # ── ⚠ POR QUÉ 0.45.1 Y NO LA ÚLTIMA, QUE ES LO MÁS IMPORTANTE DE ESTA RECETA ────────────────── # La release vigente al 2026-10-13 es **0.46.0**, y **no entra**: declara `rust-version = "1.97.1"` # y el lab de takana trae **rustc 1.97.0** (`docs/state/lab-toolchain.lock`: `rust-1.97.0-r0`). # Son dos números de parche y es un muro duro: cargo se niega antes de compilar nada. # # El salto es de upstream y es brusco —0.45.1 pedía **1.89**, o sea ocho versiones menos— así que # esto no es «quedarse atrás»: es el techo del lab, que es [[cargo-recipe-msrv-ceiling]]. Y subir el # lab **no es una opción local**: el lab entra en `hash_inputs`, así que un `bootstrap-devfs.sh # --relock` por una receta re-hashea el corpus entero. Cuando el lab suba por otra razón, esta receta # puede ir a 0.46.0 cambiando dos líneas. name = "jj" version = "0.45.1" license = "Apache-2.0" [source] # `v0.45.1`. Se pinea el SHA del tag: un tag se mueve. repo = "https://github.com/jj-vcs/jj" commit = "7c41cdeb16b6b321c64e789a966b6adf723816a5" # No committea `vendor/` ni usa `[patch.crates-io]` ⇒ el vendoreo va al `vendor/` por defecto sin # pisarle nada (la trampa que mató a `pdfcraft`; comprobado en el árbol del tag). [build] compiler = "zig-cc" target = "x86_64-linux-musl" # `static`: es una herramienta de línea de comandos y no carga nada por `dlopen`. link = "static" # Los features por defecto de `jj-cli` son `["watchman", "git"]` y se dejan: `git` es la razón de # existir del binario acá, y `watchman` es sólo un CLIENTE en Rust del vigía de ficheros de Meta —si # el demonio no está, la función queda apagada y no pasa nada. flags = ["-p", "jj-cli", "--bin", "jj"] [build.phases] compile = ''' set -e # El envoltorio de `AR` no es decorativo: el default de zig-cc exporta `AR="zig ar"` CON ESPACIO y el # crate `cc` —que acá compila `libmimalloc-sys`— ejecuta `$AR` como un binario ÚNICO ⇒ busca uno # llamado «zig ar» y falla. Cicatriz de `mise.toml`. printf '%s\n' '#!/bin/sh' 'for a do' 'case "$a" in --target=*) a=--target=x86_64-linux-musl ;; esac' 'set -- "$@" "$a"' 'shift' 'done' 'exec zig cc -mcpu=baseline "$@"' > "$PWD/.hammer-zig-cc" chmod +x "$PWD/.hammer-zig-cc" printf '%s\n' '#!/bin/sh' 'exec zig ar "$@"' > "$PWD/.hammer-zig-ar" chmod +x "$PWD/.hammer-zig-ar" RF="-C linker=$PWD/.hammer-zig-cc" rustc -vV | grep -q 'host: .*-alpine-' || RF="$RF -C link-self-contained=no" CC="$PWD/.hammer-zig-cc" AR="$PWD/.hammer-zig-ar" RUSTFLAGS="$RF" cargo rustc --release --locked --offline -p jj-cli --bin jj -- -C target-feature=+crt-static -C relocation-model=static ''' install = ''' set -e test -x target/release/jj || { echo "no hay target/release/jj — ¿cambió el nombre del bin?" >&2; exit 1; } # ── LA PRUEBA DE QUE NO ES UN NOMBRE: QUE CREE UN REPOSITORIO Y LO LEA ──────────────────────── # `--version` sólo prueba que el binario arranca. Lo que se mide acá es que el **backend de git en # Rust** funciona de verdad: crear un repo, escribir un fichero y que `jj` lo vea como cambio. Si # eso no pasa, esta receta no debe sellar (memoria: «subcomando SIN DRIVER = binario inerte»). export HOME="$PWD/.smoke/home" JJ_USER="takana" JJ_EMAIL="takana@localhost" JJ_CONFIG="$PWD/.smoke/jj.toml" mkdir -p "$HOME" "$PWD/.smoke/repo" : > "$JJ_CONFIG" ./target/release/jj --version > "$PWD/.smoke/v" 2>&1 || { echo "jj --version falló:" >&2; cat "$PWD/.smoke/v" >&2; exit 1; } grep -q "^jj 0.45.1" "$PWD/.smoke/v" || { echo "jj dice '$(cat "$PWD/.smoke/v")' y la receta pinea 0.45.1" >&2; exit 1; } ./target/release/jj git init "$PWD/.smoke/repo" > "$PWD/.smoke/init" 2>&1 || { echo "jj git init falló:" >&2; cat "$PWD/.smoke/init" >&2; exit 1; } test -d "$PWD/.smoke/repo/.jj" || { echo "jj git init salió 0 y NO dejó .jj — el caso «sale bien y no hizo nada»" >&2; exit 1; } echo hola > "$PWD/.smoke/repo/f.txt" ./target/release/jj -R "$PWD/.smoke/repo" status > "$PWD/.smoke/st" 2>&1 || { echo "jj status falló:" >&2; cat "$PWD/.smoke/st" >&2; exit 1; } grep -q "f.txt" "$PWD/.smoke/st" || { echo "jj status no vio el fichero nuevo:" >&2; cat "$PWD/.smoke/st" >&2; exit 1; } echo "smoke: jj creó el repo, vio el cambio, y todo con gitoxide (sin libgit2)" install -Dm755 target/release/jj /out/usr/bin/jj install -Dm644 LICENSE /out/usr/share/licenses/jj/LICENSE # Completado de shell y manual, generados por el propio binario. ⚠ A diferencia de todo lo de # arriba, un fallo acá NO tumba el build y es deliberado: el contrato de esta receta es el binario; # el completado es comodidad. Si el subcomando cambia de nombre en un bump, se quiere ver el aviso # en el log, no perder el artefacto entero. for sh in bash zsh fish; do if ./target/release/jj util completion "$sh" > "$PWD/.smoke/jj.$sh" 2>/dev/null && [ -s "$PWD/.smoke/jj.$sh" ]; then case "$sh" in bash) install -Dm644 "$PWD/.smoke/jj.$sh" /out/usr/share/bash-completion/completions/jj ;; zsh) install -Dm644 "$PWD/.smoke/jj.$sh" /out/usr/share/zsh/site-functions/_jj ;; fish) install -Dm644 "$PWD/.smoke/jj.$sh" /out/usr/share/fish/vendor_completions.d/jj.fish ;; esac else echo "aviso: 'jj util completion $sh' no produjo nada — ¿cambió el subcomando? el binario SÍ está" >&2 fi done test -s /out/usr/bin/jj ''' [deps] # Ninguna, y es medido: sin `libgit2-sys`, sin `openssl-sys`, sin `zlib` del sistema. `libmimalloc-sys` # compila su propio C con el crate `cc`. Un `[deps]` vacío acá significa «no necesita nada». build = []