ninja_manifest 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +9 -0
- data/LICENSE +21 -0
- data/README.md +92 -0
- data/Rakefile +16 -0
- data/lib/ninja_manifest.rb +986 -0
- data/ninja_manifest.gemspec +37 -0
- metadata +54 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f855fde81252e265a37c914cf042a3e3a7a3451f75a36355665e365a9269419d
|
|
4
|
+
data.tar.gz: 3bad76faf0a801c1036ce7e7d8fc0cf5742266898c6be3afe71d6f9d52a9cfd1
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9fd4be559c1a411c77747516bb37bcd9946451fb2d8a1b6aa08c30ce6c399cb298ab68b4f09876ff98fc84c95f81aef9409b4461a87f0fe84b156eb34772e5d8
|
|
7
|
+
data.tar.gz: 3c05281ab962527d0101b9de91266d88f3ce9ef246fbcdd148c13267898bfc20bbdb1652770d6f8681b0868f95fcff0c8ab2d8ac51b2cc1ad0f46b7eb77fb7e7
|
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Yuta Saito
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# NinjaManifest
|
|
2
|
+
|
|
3
|
+
A Ruby toolkit for parsing and evaluating [Ninja build manifest](https://ninja-build.org/manual.html) files (`build.ninja`).
|
|
4
|
+
|
|
5
|
+
Features:
|
|
6
|
+
|
|
7
|
+
* Parses Ninja build manifest files according to the official [Ninja implementation](https://ninja-build.org/)
|
|
8
|
+
* Evaluates variable expansions and rule bindings
|
|
9
|
+
* Visitor pattern API for flexible processing
|
|
10
|
+
* Compact and plain old pure Ruby implementation without external dependencies
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
Install the gem and add to the application's Gemfile by executing:
|
|
15
|
+
|
|
16
|
+
$ bundle add ninja_manifest
|
|
17
|
+
|
|
18
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
|
19
|
+
|
|
20
|
+
$ gem install ninja_manifest
|
|
21
|
+
|
|
22
|
+
## Basic Usage
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
require "ninja_manifest"
|
|
26
|
+
|
|
27
|
+
# Parse and evaluate a build.ninja content
|
|
28
|
+
manifest = NinjaManifest.load(<<~NINJA)
|
|
29
|
+
cxx = c++
|
|
30
|
+
builddir = build
|
|
31
|
+
|
|
32
|
+
rule cxx
|
|
33
|
+
command = $cxx -c $in -o $out
|
|
34
|
+
description = CXX $out
|
|
35
|
+
|
|
36
|
+
build $builddir/main.o: cxx src/main.cc
|
|
37
|
+
cxx = clang
|
|
38
|
+
NINJA
|
|
39
|
+
# Or load from build.ninja file
|
|
40
|
+
manifest = NinjaManifest.load_file("build.ninja")
|
|
41
|
+
|
|
42
|
+
# Access parsed data
|
|
43
|
+
|
|
44
|
+
manifest.variables # Variables hash
|
|
45
|
+
manifest.variables["cxx"] # => "c++"
|
|
46
|
+
|
|
47
|
+
manifest.rules # Rules hash
|
|
48
|
+
rule = manifest.rules["cxx"]
|
|
49
|
+
rule["command"] # => "$cxx -c $in -o $out"
|
|
50
|
+
rule["description"] # => "CXX $out"
|
|
51
|
+
|
|
52
|
+
build = manifest.builds.first
|
|
53
|
+
build[:outputs][:explicit] # => ["build/main.o"]
|
|
54
|
+
build[:inputs][:explicit] # => ["src/main.cc"]
|
|
55
|
+
build[:vars]["command"] # => "c++ -c src/main.cc -o build/main.o"
|
|
56
|
+
build[:vars]["description"] # => "CXX build/main.o"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Custom Visitor Usage
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
require "ninja_manifest"
|
|
63
|
+
|
|
64
|
+
# Create a custom visitor to process parsed constructs
|
|
65
|
+
class MyVisitor < NinjaManifest::Visitor
|
|
66
|
+
def visit_variable(name:, value:)
|
|
67
|
+
puts "Variable: #{name} = #{value}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def visit_rule(name:, vars:)
|
|
71
|
+
puts "Rule: #{name} with #{vars.keys.size} attributes"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def visit_build(rule:, outs:, ins:, vars:, **kwargs)
|
|
75
|
+
puts "Build: #{rule} -> #{outs[:explicit].join(', ')}"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
visitor = MyVisitor.new
|
|
80
|
+
File.open("build.ninja", "r") do |f|
|
|
81
|
+
NinjaManifest::parse(f.read, visitor)
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Contributing
|
|
86
|
+
|
|
87
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/kateinoigakukun/ninja_manifest.
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
92
|
+
|
data/Rakefile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require "bundler/gem_tasks"
|
|
2
|
+
require "rake/testtask"
|
|
3
|
+
require "rdoc/task"
|
|
4
|
+
|
|
5
|
+
Rake::TestTask.new(:test) do |t|
|
|
6
|
+
t.libs << "test"
|
|
7
|
+
t.libs << "lib"
|
|
8
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
Rake::RDocTask.new do |rdoc|
|
|
12
|
+
rdoc.rdoc_files.add(%w[README.md LICENSE lib/ninja_manifest.rb])
|
|
13
|
+
rdoc.main = "README.md"
|
|
14
|
+
rdoc.title = "ninja_parser Docs"
|
|
15
|
+
rdoc.rdoc_dir = "doc"
|
|
16
|
+
end
|
|
@@ -0,0 +1,986 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2025 Yuta Saito
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
# copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
# SOFTWARE.
|
|
22
|
+
|
|
23
|
+
##
|
|
24
|
+
# The +NinjaManifest+ module provides a simple yet robust mechanism for parsing and
|
|
25
|
+
# evaluating build manifests written for the Ninja build system. It expands variables, resolves rule
|
|
26
|
+
# bindings, and constructs a fully-evaluated manifest representation suitable for inspection or custom
|
|
27
|
+
# tooling.
|
|
28
|
+
#
|
|
29
|
+
# == What’s Here
|
|
30
|
+
#
|
|
31
|
+
# - +NinjaManifest.load+ and +NinjaManifest.load_file+: entry-points for parsing/evaluating.
|
|
32
|
+
# - +NinjaManifest::parse+: flexible visitor-based parsing API without evaluation.
|
|
33
|
+
# - +NinjaManifest::Manifest+: the evaluated manifest (variables, rules, builds, etc.).
|
|
34
|
+
# - +NinjaManifest::Visitor+: base visitor class for parsed constructs.
|
|
35
|
+
#
|
|
36
|
+
module NinjaManifest
|
|
37
|
+
VERSION = "0.1.0"
|
|
38
|
+
|
|
39
|
+
##
|
|
40
|
+
# call-seq:
|
|
41
|
+
#
|
|
42
|
+
# load(input) -> Manifest
|
|
43
|
+
#
|
|
44
|
+
# Parses and evaluates a Ninja build manifest, expanding all variables and resolving rule bindings.
|
|
45
|
+
#
|
|
46
|
+
# All variable references like $foo and ${bar} are expanded, and build bindings are resolved
|
|
47
|
+
# according to Ninja's scoping rules.
|
|
48
|
+
#
|
|
49
|
+
# manifest = NinjaManifest.load(<<~NINJA)
|
|
50
|
+
# cxx = c++
|
|
51
|
+
# builddir = build
|
|
52
|
+
#
|
|
53
|
+
# rule cxx
|
|
54
|
+
# command = $cxx -c $in -o $out
|
|
55
|
+
# description = CXX $out
|
|
56
|
+
#
|
|
57
|
+
# build $builddir/main.o: cxx src/main.cc
|
|
58
|
+
# cxx = clang
|
|
59
|
+
# NINJA
|
|
60
|
+
#
|
|
61
|
+
# manifest.variables["cxx"] # => "c++"
|
|
62
|
+
# manifest.rules["cxx"] # => {"command" => "$cxx -c $in -o $out", "description" => "CXX $out"}
|
|
63
|
+
#
|
|
64
|
+
# manifest.builds.each do |build|
|
|
65
|
+
# puts build[:outputs][:explicit] # => ["build/main.o"]
|
|
66
|
+
# puts build[:command] # => "c++ -c src/main.cc -o build/main.o"
|
|
67
|
+
# end
|
|
68
|
+
#
|
|
69
|
+
# The input is a String (manifest content). To load from a file path, use NinjaManifest.load_file instead.
|
|
70
|
+
def self.load(input, **opts)
|
|
71
|
+
evaluator = Evaluator.new(**opts)
|
|
72
|
+
parse(input, evaluator)
|
|
73
|
+
evaluator.finalize
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
##
|
|
77
|
+
# call-seq:
|
|
78
|
+
#
|
|
79
|
+
# load_file(path) -> Manifest
|
|
80
|
+
#
|
|
81
|
+
# Loads and evaluates a Ninja build manifest from a file path.
|
|
82
|
+
#
|
|
83
|
+
# This is a convenience method that reads the file and calls load.
|
|
84
|
+
#
|
|
85
|
+
# manifest = NinjaManifest.load_file("build.ninja")
|
|
86
|
+
# manifest.variables["cc"] # => "gcc"
|
|
87
|
+
# manifest.builds.size # => 42
|
|
88
|
+
def self.load_file(path, **opts)
|
|
89
|
+
File.open(path, "r") { |file| load(file.read, **opts) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
##
|
|
93
|
+
# Represents an evaluated Ninja build manifest.
|
|
94
|
+
#
|
|
95
|
+
# A Manifest contains all parsed and evaluated data from a +build.ninja+ file,
|
|
96
|
+
# including expanded variables, rules, builds, and etc.
|
|
97
|
+
#
|
|
98
|
+
# manifest = NinjaManifest.load_file("build.ninja")
|
|
99
|
+
# manifest.variables["cc"] # => "gcc"
|
|
100
|
+
# manifest.rules["cc"] # => {"command" => "gcc $in -o $out", ...}
|
|
101
|
+
# manifest.builds.first # => {:rule => "cc", :outputs => {...}, ...}
|
|
102
|
+
# manifest.defaults # => ["app"]
|
|
103
|
+
#
|
|
104
|
+
# You typically create a Manifest using NinjaManifest.load_file or NinjaManifest.load.
|
|
105
|
+
class Manifest
|
|
106
|
+
##
|
|
107
|
+
# Returns the global variables hash with all values expanded.
|
|
108
|
+
#
|
|
109
|
+
# manifest.variables["cc"] # => "gcc"
|
|
110
|
+
# manifest.variables["cflags"] # => "-Wall -O2"
|
|
111
|
+
attr_reader :variables
|
|
112
|
+
|
|
113
|
+
##
|
|
114
|
+
# Returns a hash of rule definitions, keyed by rule name.
|
|
115
|
+
#
|
|
116
|
+
# Each rule is a hash of attribute names to their (raw, unevaluated) values.
|
|
117
|
+
#
|
|
118
|
+
# manifest.rules["cc"]
|
|
119
|
+
# # => {
|
|
120
|
+
# # "command" => "gcc $in -o $out",
|
|
121
|
+
# # "description" => "CC $out",
|
|
122
|
+
# # ...
|
|
123
|
+
# # }
|
|
124
|
+
attr_reader :rules
|
|
125
|
+
|
|
126
|
+
##
|
|
127
|
+
# Returns an array of build records.
|
|
128
|
+
#
|
|
129
|
+
# Each record is a hash with the following keys:
|
|
130
|
+
# - +:rule+: the rule name used for this build
|
|
131
|
+
# - +:outputs+: hash with +:explicit+ and +:implicit+ arrays (expanded paths)
|
|
132
|
+
# - +:inputs+: hash with +:explicit+, +:implicit+, +:order_only+, +:validation+ arrays (expanded paths)
|
|
133
|
+
# - +:vars+: hash of build-local variables (expanded)
|
|
134
|
+
#
|
|
135
|
+
# Example:
|
|
136
|
+
#
|
|
137
|
+
# build = manifest.builds.first
|
|
138
|
+
# build[:outputs][:explicit] # => ["build/main.o"]
|
|
139
|
+
# build[:inputs][:explicit] # => ["src/main.cc"]
|
|
140
|
+
attr_reader :builds
|
|
141
|
+
|
|
142
|
+
##
|
|
143
|
+
# Returns an array of default target paths (fully expanded).
|
|
144
|
+
#
|
|
145
|
+
# manifest.defaults # => ["all", "tests"]
|
|
146
|
+
attr_reader :defaults
|
|
147
|
+
|
|
148
|
+
##
|
|
149
|
+
# Returns a hash of pool definitions, keyed by pool name.
|
|
150
|
+
#
|
|
151
|
+
# Each pool is a hash of attribute names to their values.
|
|
152
|
+
#
|
|
153
|
+
# manifest.pools["link"]
|
|
154
|
+
# # => { "depth" => "1" }
|
|
155
|
+
attr_reader :pools
|
|
156
|
+
|
|
157
|
+
def initialize(variables:, rules:, builds:, defaults:, pools:) # :nodoc:
|
|
158
|
+
@variables = variables
|
|
159
|
+
@rules = rules
|
|
160
|
+
@builds = builds
|
|
161
|
+
@defaults = defaults
|
|
162
|
+
@pools = pools
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
##
|
|
167
|
+
# call-seq:
|
|
168
|
+
#
|
|
169
|
+
# parse(input, visitor) -> nil
|
|
170
|
+
#
|
|
171
|
+
# Parses a Ninja build manifest and invokes callbacks on the provided Visitor object.
|
|
172
|
+
#
|
|
173
|
+
# The input is a String (manifest content). The visitor receives callbacks for all parsed constructs,
|
|
174
|
+
# see Visitor for more details.
|
|
175
|
+
#
|
|
176
|
+
# class MyVisitor < NinjaManifest::Visitor
|
|
177
|
+
# def visit_build(rule:, outs:, ins:, vars:, **kwargs)
|
|
178
|
+
# puts "Build: #{rule} -> #{outs[:explicit].join(', ')}"
|
|
179
|
+
# end
|
|
180
|
+
# end
|
|
181
|
+
#
|
|
182
|
+
# visitor = MyVisitor.new
|
|
183
|
+
# NinjaManifest.parse(<<~NINJA, visitor)
|
|
184
|
+
# cxx = c++
|
|
185
|
+
# builddir = build
|
|
186
|
+
#
|
|
187
|
+
# rule cxx
|
|
188
|
+
# command = $cxx -c $in -o $out
|
|
189
|
+
# description = CXX $out
|
|
190
|
+
#
|
|
191
|
+
# build $builddir/main.o: cxx src/main.cc
|
|
192
|
+
# cxx = clang
|
|
193
|
+
# NINJA
|
|
194
|
+
def self.parse(input, visitor)
|
|
195
|
+
parser = Parser.new(input)
|
|
196
|
+
parser.parse(visitor)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
##
|
|
200
|
+
# Base visitor class for processing parsed Ninja manifest constructs.
|
|
201
|
+
#
|
|
202
|
+
# Subclass this class and override the visit methods to customize how parsed elements
|
|
203
|
+
# are processed. All methods have default no-op implementations, so you only need to
|
|
204
|
+
# override the ones you care about.
|
|
205
|
+
#
|
|
206
|
+
# class MyVisitor < NinjaManifest::Visitor
|
|
207
|
+
# def visit_build(rule:, outs:, ins:, vars:, **kwargs)
|
|
208
|
+
# puts "Build: #{rule} -> #{outs[:explicit].join(', ')}"
|
|
209
|
+
# end
|
|
210
|
+
# end
|
|
211
|
+
#
|
|
212
|
+
# visitor = MyVisitor.new
|
|
213
|
+
# NinjaManifest.parse(<<~NINJA, visitor)
|
|
214
|
+
# cxx = c++
|
|
215
|
+
# builddir = build
|
|
216
|
+
#
|
|
217
|
+
# rule cxx
|
|
218
|
+
# command = $cxx -c $in -o $out
|
|
219
|
+
# description = CXX $out
|
|
220
|
+
#
|
|
221
|
+
# build $builddir/main.o: cxx src/main.cc
|
|
222
|
+
# cxx = clang
|
|
223
|
+
# NINJA
|
|
224
|
+
class Visitor
|
|
225
|
+
##
|
|
226
|
+
# Called when a variable assignment is encountered.
|
|
227
|
+
#
|
|
228
|
+
# The value is the raw (unevaluated) string, which may contain variable references
|
|
229
|
+
# like $foo or ${bar}. Variable expansion is not performed at parse time.
|
|
230
|
+
#
|
|
231
|
+
# def visit_variable(name:, value:)
|
|
232
|
+
# puts "#{name} = #{value}" # value may be "$cc -Wall" or similar
|
|
233
|
+
# end
|
|
234
|
+
def visit_variable(name:, value:)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
##
|
|
238
|
+
# Called when a rule definition is encountered.
|
|
239
|
+
#
|
|
240
|
+
# Rule attributes are raw strings and may contain variable references.
|
|
241
|
+
# Common attributes include +"command"+, +"description"+, +"depfile"+, etc.
|
|
242
|
+
#
|
|
243
|
+
# def visit_rule(name:, vars:)
|
|
244
|
+
# puts "Rule: #{name}"
|
|
245
|
+
# puts "Command: #{vars["command"]}" # may contain "$in $out"
|
|
246
|
+
# end
|
|
247
|
+
def visit_rule(name:, vars:)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
##
|
|
251
|
+
# Called when a build statement is encountered.
|
|
252
|
+
#
|
|
253
|
+
# The +outs+ hash contains output paths with keys:
|
|
254
|
+
# - +:explicit+: explicit output paths (before |)
|
|
255
|
+
# - +:implicit+: implicit output paths (after |)
|
|
256
|
+
#
|
|
257
|
+
# The +ins+ hash contains input paths with keys:
|
|
258
|
+
# - +:explicit+: explicit dependencies (before |)
|
|
259
|
+
# - +:implicit+: implicit dependencies (after |)
|
|
260
|
+
# - +:order_only+: order-only dependencies (after ||)
|
|
261
|
+
# - +:validation+: validation dependencies (after @)
|
|
262
|
+
#
|
|
263
|
+
# The +rule+ parameter is the rule name, or an empty string for implicit phony builds.
|
|
264
|
+
# The +vars+ hash contains build-local variable assignments (raw, unevaluated).
|
|
265
|
+
#
|
|
266
|
+
# Example:
|
|
267
|
+
#
|
|
268
|
+
# def visit_build(rule:, outs:, ins:, vars:, **kwargs)
|
|
269
|
+
# puts "Building #{outs[:explicit].join(', ')} using rule #{rule}"
|
|
270
|
+
# puts "Dependencies: #{ins[:explicit].join(', ')}"
|
|
271
|
+
# end
|
|
272
|
+
def visit_build(rule:, outs:, ins:, vars:, **kwargs)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
##
|
|
276
|
+
# Called when a default statement is encountered.
|
|
277
|
+
#
|
|
278
|
+
# The targets array contains target paths (raw, unevaluated) that are marked
|
|
279
|
+
# as default build targets.
|
|
280
|
+
#
|
|
281
|
+
# def visit_default(targets:)
|
|
282
|
+
# puts "Default targets: #{targets.join(', ')}"
|
|
283
|
+
# end
|
|
284
|
+
def visit_default(targets:)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
##
|
|
288
|
+
# Called when an include statement is encountered.
|
|
289
|
+
#
|
|
290
|
+
# The path is the path to the included file (raw, unevaluated).
|
|
291
|
+
#
|
|
292
|
+
# def visit_include(path:)
|
|
293
|
+
# puts "Including file: #{path}"
|
|
294
|
+
# end
|
|
295
|
+
def visit_include(path:)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
##
|
|
299
|
+
# Called when a subninja statement is encountered.
|
|
300
|
+
#
|
|
301
|
+
# The path is the path to the subninja file (raw, unevaluated).
|
|
302
|
+
#
|
|
303
|
+
# def visit_subninja(path:)
|
|
304
|
+
# puts "Subninja file: #{path}"
|
|
305
|
+
# end
|
|
306
|
+
def visit_subninja(path:)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
##
|
|
310
|
+
# Called when a pool statement is encountered.
|
|
311
|
+
#
|
|
312
|
+
# The name is the name of the pool (raw, unevaluated).
|
|
313
|
+
#
|
|
314
|
+
# def visit_pool(name:, vars:)
|
|
315
|
+
# puts "Pool: #{name} = #{vars}"
|
|
316
|
+
# end
|
|
317
|
+
def visit_pool(name:, vars:)
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Error raised when evaluation encounters invalid data.
|
|
322
|
+
Error = Class.new(StandardError)
|
|
323
|
+
|
|
324
|
+
# Scanner for character-by-character parsing of input text.
|
|
325
|
+
class Scanner # :nodoc:
|
|
326
|
+
attr_reader :offset, :line
|
|
327
|
+
|
|
328
|
+
def initialize(buffer)
|
|
329
|
+
@buffer = buffer.force_encoding("utf-8")
|
|
330
|
+
@offset = 0
|
|
331
|
+
@line = 1
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def peek
|
|
335
|
+
return "\0" if @offset >= @buffer.length
|
|
336
|
+
|
|
337
|
+
c = @buffer[@offset]
|
|
338
|
+
if c == "\r" && @offset + 1 < @buffer.length &&
|
|
339
|
+
@buffer[@offset + 1] == "\n"
|
|
340
|
+
"\n"
|
|
341
|
+
else
|
|
342
|
+
c
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def next
|
|
347
|
+
read
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def back
|
|
351
|
+
raise Error, "back at start" if @offset == 0
|
|
352
|
+
|
|
353
|
+
@offset -= 1
|
|
354
|
+
if @offset >= 0 && @buffer[@offset] == "\n"
|
|
355
|
+
@line -= 1
|
|
356
|
+
elsif @offset > 0 && @buffer[@offset - 1] == "\r" &&
|
|
357
|
+
@buffer[@offset] == "\n"
|
|
358
|
+
@offset -= 1
|
|
359
|
+
@line -= 1
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def read
|
|
364
|
+
return "\0" if @offset >= @buffer.length
|
|
365
|
+
|
|
366
|
+
c = @buffer[@offset]
|
|
367
|
+
if c == "\r" && @offset + 1 < @buffer.length &&
|
|
368
|
+
@buffer[@offset + 1] == "\n"
|
|
369
|
+
@offset += 2
|
|
370
|
+
@line += 1
|
|
371
|
+
return "\n"
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
@offset += 1
|
|
375
|
+
@line += 1 if c == "\n"
|
|
376
|
+
c
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def skip(ch)
|
|
380
|
+
if read != ch
|
|
381
|
+
back
|
|
382
|
+
return false
|
|
383
|
+
end
|
|
384
|
+
true
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def skip_spaces
|
|
388
|
+
while skip(" ")
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def expect(ch)
|
|
393
|
+
r = read
|
|
394
|
+
if r != ch
|
|
395
|
+
back
|
|
396
|
+
raise Error, "expected #{ch.inspect}, got #{r.inspect}"
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def slice(start, ending)
|
|
401
|
+
@buffer[start...ending]
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
private_constant :Scanner
|
|
406
|
+
|
|
407
|
+
##
|
|
408
|
+
# Parser for Ninja build manifest files.
|
|
409
|
+
#
|
|
410
|
+
# Parses build.ninja files according to the Ninja manifest format specification,
|
|
411
|
+
# handling continuations, variable expansions, and all statement types. The parser
|
|
412
|
+
# uses a visitor pattern to deliver parsed constructs.
|
|
413
|
+
class Parser # :nodoc:
|
|
414
|
+
##
|
|
415
|
+
# Creates a parser for the given input source.
|
|
416
|
+
def initialize(input)
|
|
417
|
+
@scanner = Scanner.new(input)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
##
|
|
421
|
+
# Parses the manifest and invokes visitor callbacks for each parsed construct.
|
|
422
|
+
def parse(visitor)
|
|
423
|
+
loop do
|
|
424
|
+
case @scanner.peek
|
|
425
|
+
when "\0"
|
|
426
|
+
break
|
|
427
|
+
when "\n"
|
|
428
|
+
@scanner.next
|
|
429
|
+
when "#"
|
|
430
|
+
skip_comment
|
|
431
|
+
when " ", "\t"
|
|
432
|
+
raise Error, "unexpected whitespace"
|
|
433
|
+
else
|
|
434
|
+
ident = read_ident
|
|
435
|
+
skip_spaces
|
|
436
|
+
case ident
|
|
437
|
+
when "rule"
|
|
438
|
+
visitor.visit_rule(**read_rule)
|
|
439
|
+
when "build"
|
|
440
|
+
visitor.visit_build(**read_build)
|
|
441
|
+
when "default"
|
|
442
|
+
visitor.visit_default(**read_default)
|
|
443
|
+
when "include"
|
|
444
|
+
path = read_eval(false)
|
|
445
|
+
visitor.visit_include(path: path)
|
|
446
|
+
when "subninja"
|
|
447
|
+
path = read_eval(false)
|
|
448
|
+
visitor.visit_subninja(path: path)
|
|
449
|
+
when "pool"
|
|
450
|
+
visitor.visit_pool(**read_pool)
|
|
451
|
+
else
|
|
452
|
+
# Variable assignment
|
|
453
|
+
val = read_vardef
|
|
454
|
+
visitor.visit_variable(name: ident, value: val)
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
private
|
|
461
|
+
|
|
462
|
+
def skip_comment
|
|
463
|
+
loop do
|
|
464
|
+
case @scanner.read
|
|
465
|
+
when "\0"
|
|
466
|
+
@scanner.back
|
|
467
|
+
break
|
|
468
|
+
when "\n"
|
|
469
|
+
break
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def read_ident
|
|
475
|
+
start = @scanner.offset
|
|
476
|
+
while (c = @scanner.read)
|
|
477
|
+
break unless c.match?(/[a-zA-Z0-9_._-]/)
|
|
478
|
+
break if c == "\0"
|
|
479
|
+
end
|
|
480
|
+
@scanner.back
|
|
481
|
+
ending = @scanner.offset
|
|
482
|
+
raise Error, "failed to scan ident" if ending == start
|
|
483
|
+
@scanner.slice(start, ending)
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def read_vardef
|
|
487
|
+
skip_spaces
|
|
488
|
+
@scanner.expect("=")
|
|
489
|
+
skip_spaces
|
|
490
|
+
if @scanner.peek == "\n"
|
|
491
|
+
@scanner.expect("\n")
|
|
492
|
+
return ""
|
|
493
|
+
end
|
|
494
|
+
result = read_eval(false)
|
|
495
|
+
@scanner.expect("\n")
|
|
496
|
+
result
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def read_scoped_vars(variable_name_validator: nil)
|
|
500
|
+
vars = {}
|
|
501
|
+
while @scanner.peek == " "
|
|
502
|
+
skip_spaces
|
|
503
|
+
name = read_ident
|
|
504
|
+
if variable_name_validator && !variable_name_validator.call(name)
|
|
505
|
+
raise Error, "unexpected variable #{name.inspect}"
|
|
506
|
+
end
|
|
507
|
+
skip_spaces
|
|
508
|
+
val = read_vardef
|
|
509
|
+
vars[name] = val
|
|
510
|
+
end
|
|
511
|
+
vars
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def read_rule
|
|
515
|
+
name = read_ident
|
|
516
|
+
@scanner.expect("\n")
|
|
517
|
+
validator =
|
|
518
|
+
lambda do |var|
|
|
519
|
+
%w[
|
|
520
|
+
command
|
|
521
|
+
depfile
|
|
522
|
+
dyndep
|
|
523
|
+
description
|
|
524
|
+
deps
|
|
525
|
+
generator
|
|
526
|
+
pool
|
|
527
|
+
restat
|
|
528
|
+
rspfile
|
|
529
|
+
rspfile_content
|
|
530
|
+
msvc_deps_prefix
|
|
531
|
+
hide_success
|
|
532
|
+
hide_progress
|
|
533
|
+
].include?(var)
|
|
534
|
+
end
|
|
535
|
+
vars = read_scoped_vars(variable_name_validator: validator)
|
|
536
|
+
{ name: name, vars: vars }
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def read_pool
|
|
540
|
+
name = read_ident
|
|
541
|
+
@scanner.expect("\n")
|
|
542
|
+
validator = lambda { |var| var == "depth" }
|
|
543
|
+
vars = read_scoped_vars(variable_name_validator: validator)
|
|
544
|
+
{ name: name, vars: vars }
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def read_unevaluated_paths_to(stop_at_path_sep: true)
|
|
548
|
+
skip_spaces
|
|
549
|
+
v = []
|
|
550
|
+
while !matches?(@scanner.peek, ":", "|", "\n")
|
|
551
|
+
v << read_eval(stop_at_path_sep)
|
|
552
|
+
skip_spaces
|
|
553
|
+
end
|
|
554
|
+
v
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def matches?(ch, *chars)
|
|
558
|
+
chars.include?(ch)
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def read_build
|
|
562
|
+
line = @scanner.line
|
|
563
|
+
outs_explicit = read_unevaluated_paths_to(stop_at_path_sep: true)
|
|
564
|
+
|
|
565
|
+
outs_implicit = []
|
|
566
|
+
if @scanner.peek == "|"
|
|
567
|
+
@scanner.next
|
|
568
|
+
outs_implicit = read_unevaluated_paths_to(stop_at_path_sep: true)
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
@scanner.expect(":")
|
|
572
|
+
skip_spaces
|
|
573
|
+
rule = read_ident
|
|
574
|
+
|
|
575
|
+
ins_explicit = read_unevaluated_paths_to(stop_at_path_sep: true)
|
|
576
|
+
|
|
577
|
+
ins_implicit = []
|
|
578
|
+
if @scanner.peek == "|"
|
|
579
|
+
@scanner.next
|
|
580
|
+
peek = @scanner.peek
|
|
581
|
+
if peek == "|" || peek == "@"
|
|
582
|
+
@scanner.back
|
|
583
|
+
else
|
|
584
|
+
ins_implicit = read_unevaluated_paths_to(stop_at_path_sep: true)
|
|
585
|
+
end
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
ins_order_only = []
|
|
589
|
+
if @scanner.peek == "|"
|
|
590
|
+
@scanner.next
|
|
591
|
+
if @scanner.peek == "@"
|
|
592
|
+
@scanner.back
|
|
593
|
+
else
|
|
594
|
+
@scanner.expect("|")
|
|
595
|
+
ins_order_only = read_unevaluated_paths_to(stop_at_path_sep: true)
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
ins_validation = []
|
|
600
|
+
if @scanner.peek == "|"
|
|
601
|
+
@scanner.next
|
|
602
|
+
@scanner.expect("@")
|
|
603
|
+
ins_validation = read_unevaluated_paths_to(stop_at_path_sep: true)
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
@scanner.expect("\n")
|
|
607
|
+
vars = read_scoped_vars(variable_name_validator: lambda { |_| true })
|
|
608
|
+
|
|
609
|
+
{
|
|
610
|
+
rule: rule,
|
|
611
|
+
line: line,
|
|
612
|
+
outs: {
|
|
613
|
+
explicit: outs_explicit,
|
|
614
|
+
implicit: outs_implicit
|
|
615
|
+
},
|
|
616
|
+
ins: {
|
|
617
|
+
explicit: ins_explicit,
|
|
618
|
+
implicit: ins_implicit,
|
|
619
|
+
order_only: ins_order_only,
|
|
620
|
+
validation: ins_validation
|
|
621
|
+
},
|
|
622
|
+
vars: vars
|
|
623
|
+
}
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
def read_default
|
|
627
|
+
defaults = read_unevaluated_paths_to(stop_at_path_sep: true)
|
|
628
|
+
raise Error, "expected path" if defaults.empty?
|
|
629
|
+
@scanner.expect("\n")
|
|
630
|
+
{ targets: defaults }
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
def read_eval(stop_at_path_sep)
|
|
634
|
+
result = +""
|
|
635
|
+
start = @scanner.offset
|
|
636
|
+
consumed = false
|
|
637
|
+
|
|
638
|
+
if stop_at_path_sep
|
|
639
|
+
loop do
|
|
640
|
+
ch = @scanner.read
|
|
641
|
+
case ch
|
|
642
|
+
when "\0"
|
|
643
|
+
raise Error, "unexpected EOF"
|
|
644
|
+
when " ", ":", "|", "\n"
|
|
645
|
+
@scanner.back
|
|
646
|
+
break
|
|
647
|
+
when "$"
|
|
648
|
+
# Append literal part before $
|
|
649
|
+
if @scanner.offset > start + 1
|
|
650
|
+
result << @scanner.slice(start, @scanner.offset - 1)
|
|
651
|
+
end
|
|
652
|
+
# Handle escape sequence
|
|
653
|
+
append_escape(result)
|
|
654
|
+
start = @scanner.offset
|
|
655
|
+
consumed = true
|
|
656
|
+
else
|
|
657
|
+
consumed = true
|
|
658
|
+
end
|
|
659
|
+
end
|
|
660
|
+
else
|
|
661
|
+
loop do
|
|
662
|
+
ch = @scanner.read
|
|
663
|
+
case ch
|
|
664
|
+
when "\0"
|
|
665
|
+
raise Error, "unexpected EOF"
|
|
666
|
+
when "\n"
|
|
667
|
+
@scanner.back
|
|
668
|
+
break
|
|
669
|
+
when "$"
|
|
670
|
+
# Append literal part before $
|
|
671
|
+
if @scanner.offset > start + 1
|
|
672
|
+
result << @scanner.slice(start, @scanner.offset - 1)
|
|
673
|
+
end
|
|
674
|
+
# Handle escape sequence
|
|
675
|
+
append_escape(result)
|
|
676
|
+
start = @scanner.offset
|
|
677
|
+
consumed = true
|
|
678
|
+
else
|
|
679
|
+
consumed = true
|
|
680
|
+
end
|
|
681
|
+
end
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
# Append remaining literal part
|
|
685
|
+
if @scanner.offset > start
|
|
686
|
+
result << @scanner.slice(start, @scanner.offset)
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
raise Error, "Expected a string" unless consumed
|
|
690
|
+
|
|
691
|
+
result
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
def read_simple_varname
|
|
695
|
+
start = @scanner.offset
|
|
696
|
+
while (c = @scanner.read)
|
|
697
|
+
break unless c.match?(/[a-zA-Z0-9_-]/)
|
|
698
|
+
break if c == "\0"
|
|
699
|
+
end
|
|
700
|
+
@scanner.back
|
|
701
|
+
ending = @scanner.offset
|
|
702
|
+
raise Error, "failed to scan variable name" if ending == start
|
|
703
|
+
@scanner.slice(start, ending)
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
def append_escape(result)
|
|
707
|
+
case @scanner.read
|
|
708
|
+
when "\n"
|
|
709
|
+
@scanner.skip_spaces
|
|
710
|
+
# Line continuation: $ at end of line, do nothing
|
|
711
|
+
when " ", "$", ":"
|
|
712
|
+
# Literal character
|
|
713
|
+
result << @scanner.slice(@scanner.offset - 1, @scanner.offset)
|
|
714
|
+
when "{"
|
|
715
|
+
# ${var} form
|
|
716
|
+
result << "$"
|
|
717
|
+
result << "{"
|
|
718
|
+
start = @scanner.offset
|
|
719
|
+
loop do
|
|
720
|
+
case @scanner.read
|
|
721
|
+
when "\0"
|
|
722
|
+
raise Error, "unexpected EOF"
|
|
723
|
+
when "}"
|
|
724
|
+
result << @scanner.slice(start, @scanner.offset - 1)
|
|
725
|
+
result << "}"
|
|
726
|
+
break
|
|
727
|
+
end
|
|
728
|
+
end
|
|
729
|
+
else
|
|
730
|
+
# $var form
|
|
731
|
+
@scanner.back
|
|
732
|
+
result << "$"
|
|
733
|
+
var = read_simple_varname
|
|
734
|
+
result << var
|
|
735
|
+
end
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
def skip_spaces
|
|
739
|
+
loop do
|
|
740
|
+
case @scanner.read
|
|
741
|
+
when " "
|
|
742
|
+
when "$"
|
|
743
|
+
if @scanner.peek == "\n"
|
|
744
|
+
@scanner.read # consume newline and continue loop to skip leading spaces on next line
|
|
745
|
+
else
|
|
746
|
+
@scanner.back
|
|
747
|
+
break
|
|
748
|
+
end
|
|
749
|
+
else
|
|
750
|
+
@scanner.back
|
|
751
|
+
break
|
|
752
|
+
end
|
|
753
|
+
end
|
|
754
|
+
end
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
private_constant :Parser
|
|
758
|
+
|
|
759
|
+
# Evaluator visitor that parses and fully evaluates Ninja manifests.
|
|
760
|
+
class Evaluator < Visitor # :nodoc:
|
|
761
|
+
def initialize(**opts)
|
|
762
|
+
# Global vars stack: array of simple hash of evaluated strings
|
|
763
|
+
# The last element of the stack is the current global vars.
|
|
764
|
+
@global_vars_stack = [{}]
|
|
765
|
+
# Rules: simple hash of evaluated strings
|
|
766
|
+
@rules = { "phony" => {} }
|
|
767
|
+
# Returns an array of build records.
|
|
768
|
+
@builds = []
|
|
769
|
+
# Returns an array of default target paths (fully expanded).
|
|
770
|
+
@defaults = []
|
|
771
|
+
# Returns a hash of pool definitions.
|
|
772
|
+
@pools = {}
|
|
773
|
+
@file_opener =
|
|
774
|
+
opts[:file_opener] ||
|
|
775
|
+
->(path, mode, &block) { File.open(path, mode, &block) }
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
def visit_variable(name:, value:)
|
|
779
|
+
# Evaluate immediately with current global vars
|
|
780
|
+
global_env = lambda { |key| @global_vars_stack.last[key] }
|
|
781
|
+
evaluated = expand(value, [global_env])
|
|
782
|
+
@global_vars_stack.last[name] = evaluated
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
def visit_rule(name:, vars:)
|
|
786
|
+
@rules[name] = vars.transform_values { |val| val.nil? ? nil : val.dup }
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
def visit_default(targets:)
|
|
790
|
+
global_env = lambda { |key| @global_vars_stack.last[key] }
|
|
791
|
+
expand_env = [global_env]
|
|
792
|
+
|
|
793
|
+
expanded_targets = targets.map { |target| expand(target, expand_env) }
|
|
794
|
+
@defaults.concat(expanded_targets)
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
def visit_pool(name:, vars:)
|
|
798
|
+
# Evaluate pool variables with global env
|
|
799
|
+
global_env = lambda { |key| @global_vars_stack.last[key] }
|
|
800
|
+
evaluated_vars = {}
|
|
801
|
+
vars.each do |key, value|
|
|
802
|
+
evaluated_vars[key] = expand(value, [global_env]) if value
|
|
803
|
+
end
|
|
804
|
+
@pools[name] = evaluated_vars
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
def visit_include(path:)
|
|
808
|
+
# Expand path variable references
|
|
809
|
+
global_env = lambda { |key| @global_vars_stack.last[key] }
|
|
810
|
+
expanded_path = expand(path, [global_env])
|
|
811
|
+
@file_opener.call(expanded_path, "r") do |file|
|
|
812
|
+
# Parse the included file with the current evaluator as the visitor
|
|
813
|
+
NinjaManifest.parse(file.read, self)
|
|
814
|
+
end
|
|
815
|
+
end
|
|
816
|
+
|
|
817
|
+
def visit_subninja(path:)
|
|
818
|
+
global_env = lambda { |key| @global_vars_stack.last[key] }
|
|
819
|
+
expanded_path = expand(path, [global_env])
|
|
820
|
+
|
|
821
|
+
# Push a new global vars hash onto the stack
|
|
822
|
+
@global_vars_stack.push({})
|
|
823
|
+
begin
|
|
824
|
+
@file_opener.call(expanded_path, "r") do |file|
|
|
825
|
+
# Parse the subninja file with the current evaluator as the visitor
|
|
826
|
+
NinjaManifest.parse(file.read, self)
|
|
827
|
+
end
|
|
828
|
+
ensure
|
|
829
|
+
# Pop the global vars hash from the stack
|
|
830
|
+
@global_vars_stack.pop
|
|
831
|
+
end
|
|
832
|
+
end
|
|
833
|
+
|
|
834
|
+
def visit_build(rule:, outs:, ins:, vars:, **kwargs)
|
|
835
|
+
rule_attrs = @rules[rule]
|
|
836
|
+
raise Error, "unknown rule #{rule.inspect}" unless rule_attrs
|
|
837
|
+
|
|
838
|
+
# https://ninja-build.org/manual.html#ref_scope
|
|
839
|
+
# Variable lookup order:
|
|
840
|
+
# 1. Special built-in variables ($in, $out)
|
|
841
|
+
# 2. Build-level variables from the build block
|
|
842
|
+
# 3. Rule-level variables from the rule block
|
|
843
|
+
# 4. File-level variables from the file that the build line was in
|
|
844
|
+
# 5. Variables from files that included this file using subninja keyword
|
|
845
|
+
|
|
846
|
+
# Level 4 & 5: File-level variables (current file and parent files via subninja)
|
|
847
|
+
lookup_file_level_env =
|
|
848
|
+
lambda do |key|
|
|
849
|
+
# Search from current file (Level 4) to parent files (Level 5)
|
|
850
|
+
@global_vars_stack.reverse_each do |vars|
|
|
851
|
+
return vars[key] if vars.key?(key)
|
|
852
|
+
end
|
|
853
|
+
nil
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
# Level 3: Rule-level variables
|
|
857
|
+
lookup_rule_vars = lambda { |key| rule_attrs[key] }
|
|
858
|
+
|
|
859
|
+
# Level 2: Build-level variables (raw, unevaluated)
|
|
860
|
+
build_vars_raw = vars
|
|
861
|
+
lookup_build_vars_env = lambda { |key| build_vars_raw[key] }
|
|
862
|
+
|
|
863
|
+
# Evaluate paths using levels 2, 4, and 5
|
|
864
|
+
path_envs = [lookup_build_vars_env, lookup_file_level_env]
|
|
865
|
+
outputs = {
|
|
866
|
+
explicit: outs[:explicit].map { |val| expand(val, path_envs) },
|
|
867
|
+
implicit: outs[:implicit].map { |val| expand(val, path_envs) }
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
inputs = {
|
|
871
|
+
explicit: ins[:explicit].map { |val| expand(val, path_envs) },
|
|
872
|
+
implicit: ins[:implicit].map { |val| expand(val, path_envs) },
|
|
873
|
+
order_only: ins[:order_only].map { |val| expand(val, path_envs) },
|
|
874
|
+
validation: ins[:validation].map { |val| expand(val, path_envs) }
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
# Level 1: Special built-in variables (requires evaluated paths)
|
|
878
|
+
lookup_implicit_vars =
|
|
879
|
+
lambda do |key|
|
|
880
|
+
case key
|
|
881
|
+
when "in"
|
|
882
|
+
inputs[:explicit].join(" ")
|
|
883
|
+
when "in_newline"
|
|
884
|
+
inputs[:explicit].join("\n")
|
|
885
|
+
when "out"
|
|
886
|
+
outputs[:explicit].join(" ")
|
|
887
|
+
when "out_newline"
|
|
888
|
+
outputs[:explicit].join("\n")
|
|
889
|
+
else
|
|
890
|
+
nil
|
|
891
|
+
end
|
|
892
|
+
end
|
|
893
|
+
|
|
894
|
+
path_envs = [
|
|
895
|
+
lookup_implicit_vars,
|
|
896
|
+
lookup_build_vars_env,
|
|
897
|
+
lookup_rule_vars,
|
|
898
|
+
lookup_file_level_env
|
|
899
|
+
]
|
|
900
|
+
final_vars = {}
|
|
901
|
+
rule_attrs.each do |key, value|
|
|
902
|
+
final_vars[key] = expand(value, path_envs)
|
|
903
|
+
end
|
|
904
|
+
build_vars_raw.each do |key, value|
|
|
905
|
+
final_vars[key] = expand(value, path_envs)
|
|
906
|
+
end
|
|
907
|
+
|
|
908
|
+
record = {
|
|
909
|
+
rule: rule,
|
|
910
|
+
outputs: outputs,
|
|
911
|
+
inputs: inputs,
|
|
912
|
+
vars: final_vars
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
@builds << record
|
|
916
|
+
record
|
|
917
|
+
end
|
|
918
|
+
|
|
919
|
+
def finalize
|
|
920
|
+
Manifest.new(
|
|
921
|
+
variables: @global_vars_stack.last.dup,
|
|
922
|
+
rules: @rules.dup,
|
|
923
|
+
builds: @builds.dup,
|
|
924
|
+
defaults: @defaults.dup,
|
|
925
|
+
pools: @pools.dup
|
|
926
|
+
)
|
|
927
|
+
end
|
|
928
|
+
|
|
929
|
+
private
|
|
930
|
+
|
|
931
|
+
def expand(text, env_procs)
|
|
932
|
+
result = +""
|
|
933
|
+
i = 0
|
|
934
|
+
while i < text.length
|
|
935
|
+
char = text[i]
|
|
936
|
+
if char != "$"
|
|
937
|
+
result << char
|
|
938
|
+
i += 1
|
|
939
|
+
next
|
|
940
|
+
end
|
|
941
|
+
|
|
942
|
+
i += 1
|
|
943
|
+
break if i >= text.length
|
|
944
|
+
|
|
945
|
+
next_char = text[i]
|
|
946
|
+
|
|
947
|
+
case next_char
|
|
948
|
+
when "$", " ", ":"
|
|
949
|
+
result << next_char
|
|
950
|
+
i += 1
|
|
951
|
+
when "{"
|
|
952
|
+
i += 1
|
|
953
|
+
start = i
|
|
954
|
+
i += 1 while i < text.length && text[i] != "}"
|
|
955
|
+
name = text[start...i]
|
|
956
|
+
i += 1 if i < text.length
|
|
957
|
+
result << (expand(lookup_variable(name, env_procs) || "", env_procs))
|
|
958
|
+
else
|
|
959
|
+
start = i
|
|
960
|
+
i += 1 while i < text.length && text[i].match?(/[A-Za-z0-9_-]/)
|
|
961
|
+
name = text[start...i]
|
|
962
|
+
if name.empty?
|
|
963
|
+
result << "$"
|
|
964
|
+
else
|
|
965
|
+
result << (
|
|
966
|
+
expand(lookup_variable(name, env_procs) || "", env_procs)
|
|
967
|
+
)
|
|
968
|
+
end
|
|
969
|
+
end
|
|
970
|
+
end
|
|
971
|
+
result
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
def lookup_variable(name, env_procs)
|
|
975
|
+
env_procs.each do |env|
|
|
976
|
+
next unless env
|
|
977
|
+
|
|
978
|
+
value = env.call(name)
|
|
979
|
+
return value unless value.nil?
|
|
980
|
+
end
|
|
981
|
+
nil
|
|
982
|
+
end
|
|
983
|
+
end
|
|
984
|
+
|
|
985
|
+
private_constant :Evaluator
|
|
986
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/ninja_manifest"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "ninja_manifest"
|
|
7
|
+
spec.version = NinjaManifest::VERSION
|
|
8
|
+
spec.authors = ["Yuta Saito"]
|
|
9
|
+
spec.email = ["katei@ruby-lang.org"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "A Ninja build manifest toolkit"
|
|
12
|
+
spec.description = <<END
|
|
13
|
+
NinjaManifest is a Ninja build manifest toolkit, including a parser and evaluator.
|
|
14
|
+
END
|
|
15
|
+
spec.homepage = "https://github.com/kateinoigakukun/ninja_manifest"
|
|
16
|
+
spec.license = "MIT"
|
|
17
|
+
spec.required_ruby_version = ">= 3.1.0"
|
|
18
|
+
|
|
19
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
20
|
+
spec.metadata[
|
|
21
|
+
"source_code_uri"
|
|
22
|
+
] = "https://github.com/kateinoigakukun/ninja_manifest"
|
|
23
|
+
|
|
24
|
+
# Specify which files should be added to the gem when it is released.
|
|
25
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
26
|
+
spec.files =
|
|
27
|
+
Dir.chdir(__dir__) do
|
|
28
|
+
`git ls-files -z`.split("\x0")
|
|
29
|
+
.reject do |f|
|
|
30
|
+
(f == __FILE__) ||
|
|
31
|
+
f.match(
|
|
32
|
+
%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)}
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
spec.require_paths = ["lib"]
|
|
37
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ninja_manifest
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yuta Saito
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-11-03 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: 'NinjaManifest is a Ninja build manifest toolkit, including a parser
|
|
14
|
+
and evaluator.
|
|
15
|
+
|
|
16
|
+
'
|
|
17
|
+
email:
|
|
18
|
+
- katei@ruby-lang.org
|
|
19
|
+
executables: []
|
|
20
|
+
extensions: []
|
|
21
|
+
extra_rdoc_files: []
|
|
22
|
+
files:
|
|
23
|
+
- Gemfile
|
|
24
|
+
- LICENSE
|
|
25
|
+
- README.md
|
|
26
|
+
- Rakefile
|
|
27
|
+
- lib/ninja_manifest.rb
|
|
28
|
+
- ninja_manifest.gemspec
|
|
29
|
+
homepage: https://github.com/kateinoigakukun/ninja_manifest
|
|
30
|
+
licenses:
|
|
31
|
+
- MIT
|
|
32
|
+
metadata:
|
|
33
|
+
homepage_uri: https://github.com/kateinoigakukun/ninja_manifest
|
|
34
|
+
source_code_uri: https://github.com/kateinoigakukun/ninja_manifest
|
|
35
|
+
post_install_message:
|
|
36
|
+
rdoc_options: []
|
|
37
|
+
require_paths:
|
|
38
|
+
- lib
|
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: 3.1.0
|
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '0'
|
|
49
|
+
requirements: []
|
|
50
|
+
rubygems_version: 3.5.3
|
|
51
|
+
signing_key:
|
|
52
|
+
specification_version: 4
|
|
53
|
+
summary: A Ninja build manifest toolkit
|
|
54
|
+
test_files: []
|