Skip to content

Commit ba08481

Browse files
authored
Merge pull request #3642 from dmoody256/ninja-generation
[WIP] Ninja generation tool - BETA version
2 parents 7486756 + 0b866fc commit ba08481

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+4453
-639
lines changed

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER
3535
- Fix versioned shared library naming for MacOS platform. (Previously was libxyz.dylib.1.2.3,
3636
has been fixed to libxyz.1.2.3.dylib. Additionally the sonamed symlink had the same issue,
3737
that is now resolved as well)
38+
- Add experimental ninja builder. (Contributed by MongoDB, Daniel Moody and many others).
3839
- Fix #3955 - _LIBDIRFLAGS leaving $( and $) in *COMSTR output. Added affect_signature flag to
3940
_concat function. If set to False, it will prepend and append $( and $). That way the various
4041
Environment variables can use that rather than "$( _concat(...) $)".

SCons/Node/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,11 @@ def is_conftest(self):
946946
return False
947947
return True
948948

949+
def check_attributes(self, name):
950+
""" Simple API to check if the node.attributes for name has been set"""
951+
return getattr(getattr(self, "attributes", None), name, None)
952+
953+
949954
def alter_targets(self):
950955
"""Return a list of alternate targets for this Node.
951956
"""

SCons/SConf.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -606,7 +606,7 @@ def TryBuild(self, builder, text=None, extension=""):
606606

607607
f = "_".join([f, textSig, textSigCounter])
608608
textFile = self.confdir.File(f + extension)
609-
self._set_conftest_node(sourcetext)
609+
self._set_conftest_node(textFile)
610610
textFileNode = self.env.SConfSourceBuilder(target=textFile,
611611
source=sourcetext)
612612
nodesToBeBuilt.extend(textFileNode)
@@ -625,6 +625,7 @@ def TryBuild(self, builder, text=None, extension=""):
625625
pref = self.env.subst( builder.builder.prefix )
626626
suff = self.env.subst( builder.builder.suffix )
627627
target = self.confdir.File(pref + f + suff)
628+
self._set_conftest_node(target)
628629

629630
try:
630631
# Slide our wrapper into the construction environment as

SCons/Script/SConsOptions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939

4040
diskcheck_all = SCons.Node.FS.diskcheck_types()
4141

42-
experimental_features = {'warp_speed', 'transporter'}
42+
experimental_features = {'warp_speed', 'transporter', 'ninja'}
4343

4444

4545
def diskcheck_convert(value):
@@ -749,7 +749,7 @@ def experimental_callback(option, opt, value, parser):
749749
op.add_option('--experimental',
750750
dest='experimental',
751751
action='callback',
752-
default={}, # empty set
752+
default=set(), # empty set
753753
type='str',
754754
# choices=experimental_options+experimental_features,
755755
callback =experimental_callback,

SCons/Tool/ninja/Globals.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# MIT License
2+
#
3+
# Copyright The SCons Foundation
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining
6+
# a copy of this software and associated documentation files (the
7+
# "Software"), to deal in the Software without restriction, including
8+
# without limitation the rights to use, copy, modify, merge, publish,
9+
# distribute, sublicense, and/or sell copies of the Software, and to
10+
# permit persons to whom the Software is furnished to do so, subject to
11+
# the following conditions:
12+
#
13+
# The above copyright notice and this permission notice shall be included
14+
# in all copies or substantial portions of the Software.
15+
#
16+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
17+
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
18+
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23+
24+
import SCons.Action
25+
26+
NINJA_RULES = "__NINJA_CUSTOM_RULES"
27+
NINJA_POOLS = "__NINJA_CUSTOM_POOLS"
28+
NINJA_CUSTOM_HANDLERS = "__NINJA_CUSTOM_HANDLERS"
29+
NINJA_BUILD = "NINJA_BUILD"
30+
NINJA_WHEREIS_MEMO = {}
31+
NINJA_STAT_MEMO = {}
32+
__NINJA_RULE_MAPPING = {}
33+
34+
35+
# These are the types that get_command can do something with
36+
COMMAND_TYPES = (
37+
SCons.Action.CommandAction,
38+
SCons.Action.CommandGeneratorAction,
39+
)
40+
ninja_builder_initialized = False

SCons/Tool/ninja/Methods.py

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
# MIT License
2+
#
3+
# Copyright The SCons Foundation
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining
6+
# a copy of this software and associated documentation files (the
7+
# "Software"), to deal in the Software without restriction, including
8+
# without limitation the rights to use, copy, modify, merge, publish,
9+
# distribute, sublicense, and/or sell copies of the Software, and to
10+
# permit persons to whom the Software is furnished to do so, subject to
11+
# the following conditions:
12+
#
13+
# The above copyright notice and this permission notice shall be included
14+
# in all copies or substantial portions of the Software.
15+
#
16+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
17+
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
18+
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23+
24+
import os
25+
import shlex
26+
import textwrap
27+
28+
import SCons
29+
from SCons.Tool.ninja import NINJA_CUSTOM_HANDLERS, NINJA_RULES, NINJA_POOLS
30+
from SCons.Tool.ninja.Globals import __NINJA_RULE_MAPPING
31+
from SCons.Tool.ninja.Utils import get_targets_sources, get_dependencies, get_order_only, get_outputs, get_inputs, \
32+
get_rule, get_path, generate_command, get_command_env, get_comstr
33+
34+
35+
def register_custom_handler(env, name, handler):
36+
"""Register a custom handler for SCons function actions."""
37+
env[NINJA_CUSTOM_HANDLERS][name] = handler
38+
39+
40+
def register_custom_rule_mapping(env, pre_subst_string, rule):
41+
"""Register a function to call for a given rule."""
42+
SCons.Tool.ninja.Globals.__NINJA_RULE_MAPPING[pre_subst_string] = rule
43+
44+
45+
def register_custom_rule(env, rule, command, description="", deps=None, pool=None, use_depfile=False, use_response_file=False, response_file_content="$rspc"):
46+
"""Allows specification of Ninja rules from inside SCons files."""
47+
rule_obj = {
48+
"command": command,
49+
"description": description if description else "{} $out".format(rule),
50+
}
51+
52+
if use_depfile:
53+
rule_obj["depfile"] = os.path.join(get_path(env['NINJA_DIR']), '$out.depfile')
54+
55+
if deps is not None:
56+
rule_obj["deps"] = deps
57+
58+
if pool is not None:
59+
rule_obj["pool"] = pool
60+
61+
if use_response_file:
62+
rule_obj["rspfile"] = "$out.rsp"
63+
rule_obj["rspfile_content"] = response_file_content
64+
65+
env[NINJA_RULES][rule] = rule_obj
66+
67+
68+
def register_custom_pool(env, pool, size):
69+
"""Allows the creation of custom Ninja pools"""
70+
env[NINJA_POOLS][pool] = size
71+
72+
73+
def set_build_node_callback(env, node, callback):
74+
if not node.is_conftest():
75+
node.attributes.ninja_build_callback = callback
76+
77+
78+
def get_generic_shell_command(env, node, action, targets, sources, executor=None):
79+
return (
80+
"GENERATED_CMD",
81+
{
82+
"cmd": generate_command(env, node, action, targets, sources, executor=executor),
83+
"env": get_command_env(env),
84+
},
85+
# Since this function is a rule mapping provider, it must return a list of dependencies,
86+
# and usually this would be the path to a tool, such as a compiler, used for this rule.
87+
# However this function is to generic to be able to reliably extract such deps
88+
# from the command, so we return a placeholder empty list. It should be noted that
89+
# generally this function will not be used solely and is more like a template to generate
90+
# the basics for a custom provider which may have more specific options for a provider
91+
# function for a custom NinjaRuleMapping.
92+
[]
93+
)
94+
95+
96+
def CheckNinjaCompdbExpand(env, context):
97+
""" Configure check testing if ninja's compdb can expand response files"""
98+
99+
# TODO: When would this be false?
100+
context.Message('Checking if ninja compdb can expand response files... ')
101+
ret, output = context.TryAction(
102+
action='ninja -f $SOURCE -t compdb -x CMD_RSP > $TARGET',
103+
extension='.ninja',
104+
text=textwrap.dedent("""
105+
rule CMD_RSP
106+
command = $cmd @$out.rsp > fake_output.txt
107+
description = Building $out
108+
rspfile = $out.rsp
109+
rspfile_content = $rspc
110+
build fake_output.txt: CMD_RSP fake_input.txt
111+
cmd = echo
112+
pool = console
113+
rspc = "test"
114+
"""))
115+
result = '@fake_output.txt.rsp' not in output
116+
context.Result(result)
117+
return result
118+
119+
120+
def get_command(env, node, action): # pylint: disable=too-many-branches
121+
"""Get the command to execute for node."""
122+
if node.env:
123+
sub_env = node.env
124+
else:
125+
sub_env = env
126+
executor = node.get_executor()
127+
tlist, slist = get_targets_sources(node)
128+
129+
# Generate a real CommandAction
130+
if isinstance(action, SCons.Action.CommandGeneratorAction):
131+
# pylint: disable=protected-access
132+
action = action._generate(tlist, slist, sub_env, 1, executor=executor)
133+
134+
variables = {}
135+
136+
comstr = get_comstr(sub_env, action, tlist, slist)
137+
if not comstr:
138+
return None
139+
140+
provider = __NINJA_RULE_MAPPING.get(comstr, get_generic_shell_command)
141+
rule, variables, provider_deps = provider(sub_env, node, action, tlist, slist, executor=executor)
142+
143+
# Get the dependencies for all targets
144+
implicit = list({dep for tgt in tlist for dep in get_dependencies(tgt)})
145+
146+
# Now add in the other dependencies related to the command,
147+
# e.g. the compiler binary. The ninja rule can be user provided so
148+
# we must do some validation to resolve the dependency path for ninja.
149+
for provider_dep in provider_deps:
150+
151+
provider_dep = sub_env.subst(provider_dep)
152+
if not provider_dep:
153+
continue
154+
155+
# If the tool is a node, then SCons will resolve the path later, if its not
156+
# a node then we assume it generated from build and make sure it is existing.
157+
if isinstance(provider_dep, SCons.Node.Node) or os.path.exists(provider_dep):
158+
implicit.append(provider_dep)
159+
continue
160+
161+
# in some case the tool could be in the local directory and be supplied without the ext
162+
# such as in windows, so append the executable suffix and check.
163+
prog_suffix = sub_env.get('PROGSUFFIX', '')
164+
provider_dep_ext = provider_dep if provider_dep.endswith(prog_suffix) else provider_dep + prog_suffix
165+
if os.path.exists(provider_dep_ext):
166+
implicit.append(provider_dep_ext)
167+
continue
168+
169+
# Many commands will assume the binary is in the path, so
170+
# we accept this as a possible input from a given command.
171+
172+
provider_dep_abspath = sub_env.WhereIs(provider_dep) or sub_env.WhereIs(provider_dep, path=os.environ["PATH"])
173+
if provider_dep_abspath:
174+
implicit.append(provider_dep_abspath)
175+
continue
176+
177+
# Possibly these could be ignore and the build would still work, however it may not always
178+
# rebuild correctly, so we hard stop, and force the user to fix the issue with the provided
179+
# ninja rule.
180+
raise Exception("Could not resolve path for %s dependency on node '%s'" % (provider_dep, node))
181+
182+
ninja_build = {
183+
"order_only": get_order_only(node),
184+
"outputs": get_outputs(node),
185+
"inputs": get_inputs(node),
186+
"implicit": implicit,
187+
"rule": get_rule(node, rule),
188+
"variables": variables,
189+
}
190+
191+
# Don't use sub_env here because we require that NINJA_POOL be set
192+
# on a per-builder call basis to prevent accidental strange
193+
# behavior like env['NINJA_POOL'] = 'console' and sub_env can be
194+
# the global Environment object if node.env is None.
195+
# Example:
196+
#
197+
# Allowed:
198+
#
199+
# env.Command("ls", NINJA_POOL="ls_pool")
200+
#
201+
# Not allowed and ignored:
202+
#
203+
# env["NINJA_POOL"] = "ls_pool"
204+
# env.Command("ls")
205+
#
206+
# TODO: Why not alloe env['NINJA_POOL'] ? (bdbaddog)
207+
if node.env and node.env.get("NINJA_POOL", None) is not None:
208+
ninja_build["pool"] = node.env["NINJA_POOL"]
209+
210+
return ninja_build
211+
212+
213+
def gen_get_response_file_command(env, rule, tool, tool_is_dynamic=False, custom_env={}):
214+
"""Generate a response file command provider for rule name."""
215+
216+
# If win32 using the environment with a response file command will cause
217+
# ninja to fail to create the response file. Additionally since these rules
218+
# generally are not piping through cmd.exe /c any environment variables will
219+
# make CreateProcess fail to start.
220+
#
221+
# On POSIX we can still set environment variables even for compile
222+
# commands so we do so.
223+
use_command_env = not env["PLATFORM"] == "win32"
224+
if "$" in tool:
225+
tool_is_dynamic = True
226+
227+
def get_response_file_command(env, node, action, targets, sources, executor=None):
228+
if hasattr(action, "process"):
229+
cmd_list, _, _ = action.process(targets, sources, env, executor=executor)
230+
cmd_list = [str(c).replace("$", "$$") for c in cmd_list[0]]
231+
else:
232+
command = generate_command(
233+
env, node, action, targets, sources, executor=executor
234+
)
235+
cmd_list = shlex.split(command)
236+
237+
if tool_is_dynamic:
238+
tool_command = env.subst(
239+
tool, target=targets, source=sources, executor=executor
240+
)
241+
else:
242+
tool_command = tool
243+
244+
try:
245+
# Add 1 so we always keep the actual tool inside of cmd
246+
tool_idx = cmd_list.index(tool_command) + 1
247+
except ValueError:
248+
raise Exception(
249+
"Could not find tool {} in {} generated from {}".format(
250+
tool, cmd_list, get_comstr(env, action, targets, sources)
251+
)
252+
)
253+
254+
cmd, rsp_content = cmd_list[:tool_idx], cmd_list[tool_idx:]
255+
rsp_content = ['"' + rsp_content_item + '"' for rsp_content_item in rsp_content]
256+
rsp_content = " ".join(rsp_content)
257+
258+
variables = {"rspc": rsp_content, rule: cmd}
259+
if use_command_env:
260+
variables["env"] = get_command_env(env)
261+
262+
for key, value in custom_env.items():
263+
variables["env"] += env.subst(
264+
"export %s=%s;" % (key, value), target=targets, source=sources, executor=executor
265+
) + " "
266+
return rule, variables, [tool_command]
267+
268+
return get_response_file_command

0 commit comments

Comments
 (0)