1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
|
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2022 Pierre Langlois <pierre.langlois@gmx.com>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
(define-module (guix build tree-sitter-build-system)
#:use-module ((guix build node-build-system) #:prefix node:)
#:use-module (guix build json)
#:use-module (guix build utils)
#:use-module (ice-9 match)
#:use-module (ice-9 regex)
#:use-module (srfi srfi-1)
#:export (%standard-phases
tree-sitter-build))
;; Commentary:
;;
;; Build procedures for tree-sitter grammar packages. This is the
;; builder-side code, which builds on top of the node build-system.
;;
;; Tree-sitter grammars are written in JavaScript and compiled to a native
;; shared object. The `tree-sitter generate' command invokes `node' in order
;; to evaluate the grammar.js into a grammar.json file, which is then
;; translated into C code. We then compile the C code ourselves. Packages
;; also sometimes add extra manually written C/C++ code.
;;
;; In order to support grammars depending on each other, such as C and C++,
;; JavaScript and TypeScript, this build-system installs the source of the
;; node module in a dedicated "js" output.
;;
;; Code:
(define* (patch-dependencies #:key inputs #:allow-other-keys)
"Rewrite dependencies in 'package.json'. We remove all runtime dependencies
and replace development dependencies with tree-sitter grammar node modules."
(define (rewrite package.json)
(map (match-lambda
(("dependencies" @ . _)
'("dependencies" @))
(("devDependencies" @ . _)
`("devDependencies" @
,@(filter-map (match-lambda
((key . directory)
(let ((node-module
(string-append directory
"/lib/node_modules/"
key)))
(and (directory-exists? node-module)
`(,key . ,node-module)))))
(alist-delete "node" inputs))))
(other other))
package.json))
(node:with-atomic-json-file-replacement "package.json"
(match-lambda
(('@ . package.json)
(cons '@ (rewrite package.json))))))
;; FIXME: The node build-system's configure phase does not support
;; cross-compiling so we re-define it.
(define* (configure #:key native-inputs inputs #:allow-other-keys)
(invoke (search-input-file (or native-inputs inputs) "/bin/npm")
"--offline" "--ignore-scripts" "install"))
(define* (build #:key grammar-directories #:allow-other-keys)
(for-each (lambda (dir)
(with-directory-excursion dir
;; Avoid generating binding code for other languages, we do
;; not support this use-case yet and it relies on running
;; `node-gyp' to build native addons.
(invoke "tree-sitter" "generate" "--no-bindings")))
grammar-directories))
(define* (check #:key grammar-directories tests? #:allow-other-keys)
(when tests?
(for-each (lambda (dir)
(with-directory-excursion dir
(invoke "tree-sitter" "test")))
grammar-directories)))
(define* (install #:key target grammar-directories outputs #:allow-other-keys)
(let ((lib (string-append (assoc-ref outputs "out")
"/lib/tree-sitter")))
(mkdir-p lib)
(define (compile-language dir)
(with-directory-excursion dir
(let ((lang (assoc-ref (call-with-input-file "src/grammar.json"
read-json)
"name"))
(source-file (lambda (path)
(if (file-exists? path)
path
#f))))
(apply invoke
`(,(if target
(string-append target "-g++")
"g++")
"-shared"
"-fPIC"
"-fno-exceptions"
"-O2"
"-g"
"-o" ,(string-append lib "/libtree-sitter-" lang ".so")
;; An additional `scanner.{c,cc}' file is sometimes
;; provided.
,@(cond
((source-file "src/scanner.c")
=> (lambda (file) (list "-xc" "-std=c99" file)))
((source-file "src/scanner.cc")
=> (lambda (file) (list file)))
(else '()))
"-xc" "src/parser.c")))))
(for-each compile-language grammar-directories)))
(define* (install-js #:key native-inputs inputs outputs #:allow-other-keys)
(invoke (search-input-file (or native-inputs inputs) "/bin/npm")
"--prefix" (assoc-ref outputs "js")
"--global"
"--offline"
"--loglevel" "info"
"--production"
;; Skip scripts to prevent building bindings via GYP.
"--ignore-scripts"
"install" "../package.tgz"))
(define %standard-phases
(modify-phases node:%standard-phases
(replace 'patch-dependencies patch-dependencies)
(replace 'configure configure)
(replace 'build build)
(replace 'check check)
(replace 'install install)
(add-after 'install 'install-js install-js)))
(define* (tree-sitter-build #:key inputs (phases %standard-phases)
#:allow-other-keys #:rest args)
(apply node:node-build #:inputs inputs #:phases phases args))
;;; tree-sitter-build-system.scm ends here
|