helix: build tree-sitter grammars from source (#452275)

This commit is contained in:
Matthieu Coudron
2026-02-17 18:06:18 +00:00
committed by GitHub
9 changed files with 5024 additions and 108 deletions
+1 -5
View File
@@ -25,15 +25,11 @@ rustPlatform.buildRustPackage (finalAttrs: {
env = {
# disable fetching and building of tree-sitter grammars in the helix-term build.rs
HELIX_DISABLE_AUTO_GRAMMAR_BUILD = "1";
HELIX_DEFAULT_RUNTIME = "${placeholder "out"}/lib/runtime";
HELIX_DEFAULT_RUNTIME = helix.runtime;
};
postInstall = ''
mkdir -p $out/lib
cp -r runtime $out/lib
# copy tree-sitter grammars from helix package
# TODO: build it from source instead
cp -r ${helix}/lib/runtime/grammars/* $out/lib/runtime/grammars/
installShellCompletion contrib/completion/hx.{bash,fish,zsh}
mkdir -p $out/share/{applications,icons/hicolor/256x256/apps}
cp contrib/Helix.desktop $out/share/applications
+155
View File
@@ -0,0 +1,155 @@
#!/usr/bin/env nix-shell
#! nix-shell -i python3 -p python3 nurl
"""
Generate grammar information for Helix editor by parsing languages.toml
and fetching source information using nurl in parallel.
"""
import argparse
import asyncio
import json
import os
import sys
import tomllib
from dataclasses import dataclass
from pathlib import Path
from typing import Any
@dataclass
class Grammar:
name: str
git_url: str
rev: str
subpath: str | None = None
async def run_nurl(url: str, rev: str, semaphore: asyncio.Semaphore) -> dict[str, Any]:
"""Run nurl command for a single grammar and return parsed JSON output."""
async with semaphore:
proc = await asyncio.create_subprocess_exec(
"nurl",
url,
rev,
"--json",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await proc.communicate()
if proc.returncode != 0:
raise RuntimeError(f"nurl failed for {url}@{rev}: {stderr.decode()}")
return json.loads(stdout.decode())
def parse_languages_toml(toml_path: Path) -> list[Grammar]:
"""Parse languages.toml and extract grammar information."""
with open(toml_path, "rb") as f:
config = tomllib.load(f)
grammars = []
for grammar in config.get("grammar", []):
if "source" not in grammar:
continue
source = grammar["source"]
if "git" not in source or "rev" not in source:
continue
grammars.append(
Grammar(
name=grammar["name"].replace("_", "-"),
git_url=source["git"],
rev=source["rev"],
subpath=source.get("subpath"),
)
)
return grammars
async def fetch_all_grammars(
grammars: list[Grammar], max_parallel: int
) -> dict[str, Any]:
"""Fetch nurl information for all grammars in parallel."""
semaphore = asyncio.Semaphore(max_parallel)
results = {}
total = len(grammars)
completed = 0
tasks = []
for grammar in grammars:
task = run_nurl(grammar.git_url, grammar.rev, semaphore)
tasks.append((grammar, task))
for grammar, task in tasks:
try:
result = await task
results[grammar.name] = {
"nurl": result,
"subpath": grammar.subpath,
}
completed += 1
print(f"[{completed}/{total}] ✓ {grammar.name}", file=sys.stderr)
except Exception as e:
completed += 1
print(f"[{completed}/{total}] ✗ {grammar.name}: {e}", file=sys.stderr)
results[grammar.name] = {"error": str(e)}
return results
async def main():
parser = argparse.ArgumentParser(
description="Generate grammar information for Helix editor"
)
parser.add_argument(
"languages_toml",
type=Path,
help="path to languages.toml from Helix repository",
)
parser.add_argument(
"-o",
"--output",
type=Path,
default=Path("grammars.json"),
help="output JSON file (default: grammars.json)",
)
parser.add_argument(
"-j",
"--jobs",
type=int,
default=os.cpu_count(),
help=f"number of parallel nurl instances (default: {os.cpu_count()})",
)
args = parser.parse_args()
if not args.languages_toml.exists():
print(f"Error: {args.languages_toml} not found", file=sys.stderr)
sys.exit(1)
print(f"Parsing {args.languages_toml}...", file=sys.stderr)
grammars = parse_languages_toml(args.languages_toml)
print(f"Found {len(grammars)} grammars", file=sys.stderr)
print(f"Fetching grammar information ({args.jobs} parallel jobs)...", file=sys.stderr)
results = await fetch_all_grammars(grammars, args.jobs)
errors = [name for name, data in results.items() if "error" in data]
if errors:
print(f"\nFailed grammars ({len(errors)}):", file=sys.stderr)
for name in errors:
print(f" - {name}: {results[name]['error']}", file=sys.stderr)
sys.exit(1)
with open(args.output, "w") as f:
json.dump(results, f, indent=2)
f.write('\n')
print(f"\nResults written to {args.output}", file=sys.stderr)
if __name__ == "__main__":
asyncio.run(main())
File diff suppressed because it is too large Load Diff
+140 -74
View File
@@ -1,88 +1,154 @@
{
fetchzip,
fetchpatch,
fetchFromGitHub,
lib,
rustPlatform,
mdbook,
git,
gitMinimal,
installShellFiles,
versionCheckHook,
nix-update-script,
runCommand,
removeReferencesTo,
pkgs,
tree-sitter,
lockedGrammars ? lib.importJSON ./grammars.json,
grammarsOverlay ? (
final: prev: {
tree-sitter-sql = prev.tree-sitter-sql.override {
generate = false;
};
tree-sitter-qmljs = prev.tree-sitter-qmljs.overrideAttrs {
dontCheckForBrokenSymlinks = true;
};
}
),
}:
rustPlatform.buildRustPackage (finalAttrs: {
pname = "helix";
version = "25.07.1";
outputs = [
"out"
"doc"
];
rustPlatform.buildRustPackage (
finalAttrs:
let
lockedVersionsOverlay =
final: prev:
lib.mapAttrs (
drvName: grammar:
let
lockedGrammar = lockedGrammars.${lib.removePrefix "tree-sitter-" drvName};
in
(prev.${drvName}.override {
location = lockedGrammar.subpath;
}).overrideAttrs
{
version = lib.sources.shortRev lockedGrammar.nurl.args.rev;
src = (pkgs.${lockedGrammar.nurl.fetcher} lockedGrammar.nurl.args);
}
) prev;
# This release tarball includes source code for the tree-sitter grammars,
# which is not ordinarily part of the repository.
src = fetchzip {
url = "https://github.com/helix-editor/helix/releases/download/${finalAttrs.version}/helix-${finalAttrs.version}-source.tar.xz";
hash = "sha256-Pj/lfcQXRWqBOTTWt6+Gk61F9F1UmeCYr+26hGdG974=";
stripRoot = false;
};
tree-sitter-grammars =
lib.filterAttrs (drvName: _: lib.hasAttr (lib.removePrefix "tree-sitter-" drvName) lockedGrammars)
(
tree-sitter.grammarsScope.overrideScope (
lib.composeExtensions lockedVersionsOverlay grammarsOverlay
)
);
patches = [
# Support mdbook 0.5.x: escape HTML tags in command descriptions
./mdbook-0.5-support.patch
];
# Dynamic libraries for the grammars always use the `.so` extension, also on Darwin (should use `.dylib`)
# See here: https://github.com/helix-editor/helix/pull/14982
# Switch to `stdenv.hostPlatform.extensions.sharedLibrary` once the fix above reaches the next release
postPatch = ''
# mdbook 0.5 uses asset hashing for CSS/JS files
# Remove custom theme to use default mdbook theme with correct asset references
rm -f book/theme/index.hbs
'';
grammarsFarm = runCommand "helix-grammars" { } (
lib.concatMapAttrsStringSep "\n" (_: grammar: ''
install -D ${grammar}/parser $out/${grammar.language}.so
${lib.getExe removeReferencesTo} -t ${grammar} $out/${grammar.language}.so
'') (lib.filterAttrs (_: lib.isDerivation) tree-sitter-grammars)
);
cargoHash = "sha256-Mf0nrgMk1MlZkSyUN6mlM5lmTcrOHn3xBNzmVGtApEU=";
lockedGrammarsCount = lib.length (lib.attrNames lockedGrammars);
nativeBuildInputs = [
git
installShellFiles
mdbook
];
env.HELIX_DEFAULT_RUNTIME = "${placeholder "out"}/lib/runtime";
postBuild = ''
mdbook build book -d ../book-html
'';
postInstall = ''
# not needed at runtime
rm -r runtime/grammars/sources
mkdir -p $out/lib $doc/share/doc
cp -r runtime $out/lib
installShellCompletion contrib/completion/hx.{bash,fish,zsh}
mkdir -p $out/share/{applications,icons/hicolor/256x256/apps}
cp contrib/Helix.desktop $out/share/applications
cp contrib/helix.png $out/share/icons/hicolor/256x256/apps
cp -r ../book-html $doc/share/doc/$name
'';
nativeInstallCheckInputs = [
versionCheckHook
];
versionCheckProgram = "${placeholder "out"}/bin/hx";
doInstallCheck = true;
passthru = {
updateScript = nix-update-script { };
};
meta = {
description = "Post-modern modal text editor";
homepage = "https://helix-editor.com";
changelog = "https://github.com/helix-editor/helix/blob/${finalAttrs.version}/CHANGELOG.md";
license = lib.licenses.mpl20;
mainProgram = "hx";
maintainers = with lib.maintainers; [
danth
yusdacra
runtimeDir = runCommand "helix-runtime" { } ''
cp -r --no-preserve=mode ${finalAttrs.src}/runtime $out
rm -r $out/grammars
ln -s ${grammarsFarm} $out/grammars
count=$(ls -1 "$out/grammars/" | wc -l)
if [ "$count" -ne ${toString lockedGrammarsCount} ]; then
echo "Expected ${toString lockedGrammarsCount} grammars, found $count"
exit 1
fi
'';
in
{
pname = "helix";
version = "25.07.1";
outputs = [
"out"
"doc"
];
};
})
src = fetchFromGitHub {
owner = "helix-editor";
repo = "helix";
tag = "${finalAttrs.version}";
hash = "sha256-RFSzGAcB0mMg/02ykYfTWXzQjLFu2CJ4BkS5HZ/6pBo=";
};
patches = [
# Support mdbook 0.5.x: escape HTML tags in command descriptions
./mdbook-0.5-support.patch
];
postPatch = ''
# mdbook 0.5 uses asset hashing for CSS/JS files
# Remove custom theme to use default mdbook theme with correct asset references
rm -f book/theme/index.hbs
'';
cargoHash = "sha256-Mf0nrgMk1MlZkSyUN6mlM5lmTcrOHn3xBNzmVGtApEU=";
nativeBuildInputs = [
gitMinimal
installShellFiles
mdbook
];
env = {
HELIX_DEFAULT_RUNTIME = runtimeDir;
HELIX_DISABLE_AUTO_GRAMMAR_BUILD = "1";
};
postBuild = ''
mdbook build book -d ../book-html
'';
postInstall = ''
mkdir -p $out/lib $doc/share/doc
installShellCompletion contrib/completion/hx.{bash,fish,zsh}
mkdir -p $out/share/{applications,icons/hicolor/256x256/apps}
cp contrib/Helix.desktop $out/share/applications/Helix.desktop
cp contrib/helix.png $out/share/icons/hicolor/256x256/apps/helix.png
cp -r ../book-html $doc/share/doc/$name
'';
nativeInstallCheckInputs = [
versionCheckHook
];
versionCheckProgram = "${placeholder "out"}/bin/hx";
doInstallCheck = true;
passthru = {
updateScript = ./update.sh;
runtime = runtimeDir;
inherit tree-sitter-grammars;
};
meta = {
description = "Post-modern modal text editor";
homepage = "https://helix-editor.com";
changelog = "https://github.com/helix-editor/helix/blob/${finalAttrs.version}/CHANGELOG.md";
license = lib.licenses.mpl20;
mainProgram = "hx";
maintainers = with lib.maintainers; [
aciceri
danth
yusdacra
];
};
}
)
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env nix-shell
#!nix-shell -i bash -p nurl nix-update python3
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "Updating helix source to the latest stable..."
nix-update helix
echo "Fetching updated helixSource..."
HELIX_SRC=$(nix-instantiate --eval -A "helix.src.outPath" --raw)
echo "Generating grammars.json..."
"$SCRIPT_DIR/generate_grammars.py" \
"$HELIX_SRC/languages.toml" \
-o "$SCRIPT_DIR/grammars.json"
if [ $? -ne 0 ]; then
echo "Error: Failed to generate grammars.json" >&2
exit 1
fi
echo "Done! Updated grammars.json"
@@ -1,11 +1,12 @@
{
lib,
stdenv,
newScope,
fetchFromGitHub,
fetchFromGitLab,
fetchFromSourcehut,
fetchFromCodeberg,
nix-update-script,
runCommand,
which,
rustPlatform,
emscripten,
@@ -56,6 +57,7 @@ let
fetchFromGitHub
fetchFromGitLab
fetchFromSourcehut
fetchFromCodeberg
;
};
@@ -69,6 +71,14 @@ let
*/
builtGrammars = lib.mapAttrs (_: lib.makeOverridable buildGrammar) grammars;
/**
# Extensible package set for tree-sitter grammars.
# Provides .override and .extend for customization.
# Note: Use builtGrammars (not this) when iterating over grammars,
# as this includes package set functions alongside derivations
*/
grammarsScope = lib.makeScope newScope (self: builtGrammars);
# Usage:
# pkgs.tree-sitter.withPlugins (p: [ p.tree-sitter-c p.tree-sitter-java ... ])
#
@@ -184,6 +194,7 @@ rustPlatform.buildRustPackage (finalAttrs: {
grammars
buildGrammar
builtGrammars
grammarsScope
withPlugins
allGrammars
;
@@ -3,6 +3,7 @@
fetchFromGitHub,
fetchFromGitLab,
fetchFromSourcehut,
fetchFromCodeberg,
nix-update-script,
}:
@@ -74,6 +75,7 @@ lib.mapAttrs' (
github = fetchFromGitHub;
gitlab = fetchFromGitLab;
sourcehut = fetchFromSourcehut;
codeberg = fetchFromCodeberg;
# NOTE: include other types here as required
};
in
File diff suppressed because it is too large Load Diff
+2
View File
@@ -19371,7 +19371,9 @@ self: super: with self; {
name: value:
!(builtins.elem name [
"tree-sitter-go-template"
"tree-sitter-php-only"
"tree-sitter-sql"
"tree-sitter-sshclientconfig"
"tree-sitter-templ"
])
) pkgs.tree-sitter.builtGrammars