From 35fe45e91bdca3128c873ebc7dd18f52063524c5 Mon Sep 17 00:00:00 2001 From: Googler Date: Tue, 20 Feb 2024 17:58:00 -0800 Subject: [PATCH] BEGIN_PUBLIC Implement cc_args. Also change cc_flag_set / cc_flag_group to cc_args / cc_arg_group. This is to lean into the idea that this is roughly equivalent to ctx.actions.args() END_PUBLIC PiperOrigin-RevId: 608804069 Change-Id: I74ea883b14219f904aaafc4eab902b96a1fb3e3d --- cc/toolchains/README.md | 116 +++++++++++++----- cc/toolchains/actions.bzl | 6 +- cc/toolchains/args.bzl | 105 ++++++++++++++++ cc/toolchains/cc_toolchain_info.bzl | 31 +++-- cc/toolchains/impl/BUILD | 6 + cc/toolchains/impl/collect.bzl | 55 +++++++++ tests/rule_based_toolchain/args/BUILD | 25 ++++ tests/rule_based_toolchain/args/args_test.bzl | 45 +++++++ tests/rule_based_toolchain/subjects.bzl | 30 ++--- tests/rule_based_toolchain/testdata/BUILD | 16 +++ tests/rule_based_toolchain/testdata/file1 | 0 tests/rule_based_toolchain/testdata/file2 | 0 tests/rule_based_toolchain/testdata/multiple1 | 0 tests/rule_based_toolchain/testdata/multiple2 | 0 14 files changed, 376 insertions(+), 59 deletions(-) create mode 100644 cc/toolchains/args.bzl create mode 100644 cc/toolchains/impl/BUILD create mode 100644 cc/toolchains/impl/collect.bzl create mode 100644 tests/rule_based_toolchain/args/BUILD create mode 100644 tests/rule_based_toolchain/args/args_test.bzl create mode 100644 tests/rule_based_toolchain/testdata/BUILD create mode 100644 tests/rule_based_toolchain/testdata/file1 create mode 100644 tests/rule_based_toolchain/testdata/file2 create mode 100644 tests/rule_based_toolchain/testdata/multiple1 create mode 100644 tests/rule_based_toolchain/testdata/multiple2 diff --git a/cc/toolchains/README.md b/cc/toolchains/README.md index 976dd14..a6658c1 100644 --- a/cc/toolchains/README.md +++ b/cc/toolchains/README.md @@ -36,7 +36,7 @@ sh_binary( An action config is a mapping from action to: * A list of tools, (the first one matching the execution requirements is used). -* A list of flags and features that are always enabled for the action +* A list of args and features that are always enabled for the action * A set of additional files required for the action Each action can only be specified once in the toolchain. Specifying multiple @@ -62,7 +62,7 @@ cc_action_config( name = "c_compile", actions = ["@rules_cc//actions:all_c_compile"], tools = ["@sysroot//:clang"], - flag_sets = [":my_flag_set"], + args = [":my_args"], implies = [":my_feature"], additional_files = ["@sysroot//:all_header_files"], ) @@ -74,57 +74,109 @@ cc_additional_files_for_actions( ) ``` -## Step 3: Define some flag sets -Flag sets are just sets of flags to be associated with actions. Most flag sets -are simple, so we provide the shorthand `flags`. However, sometimes you -need to do more complex things, for which we support `flag_groups` instead. - -Flag groups work exactly the same as the existing toolchain definition. - -Flag sets are a combination of both `flag_set` and `env_set` from the existing -toolchain definition. - -`cc_flag_set_list` is simply a list of flag sets. This can be used to group -flag sets together, and preserves ordering. +## Step 3: Define some arguments +Arguments are our replacement for `flag_set` and `env_set`. To add arguments to +our tools, we take heavy inspiration from bazel's +[`Args`](https://bazel.build/rules/lib/builtins/Args) type. We provide the same +API, with the following caveats: +* `actions` specifies which actions the arguments apply to (same as `flag_set`). +* `requires_any_of` is equivalent to `with_features` on the `flag_set`. +* `args` may be used instead of `add` if your command-line is only strings. +* `env` may be used to add environment variables to the arguments. Environment + variables set by later args take priority. +* By default, all inputs are automatically added to the corresponding actions. + `additional_files` specifies files that are required for an action when using + that argument. ``` -cc_flag_set( - name = "simple", +cc_args( + name = "inline", actions = ["@rules_cc//actions:all_cpp_compile_actions"], - flags = ["--foo"], - envs = {"FOO": "bar"}, + args = ["--foo"], + requires_any_of = [":feature"] + env = {"FOO": "bar"}, + additional_files = [":file"], +) +``` + +For more complex use cases, we use the same API as `Args`. Values are either: +* A list of files (or a single file for `cc_add_args`). +* Something returning `CcVariableInfo`, which is equivalent to a list of strings. + +``` +cc_variable( + name = "bar_baz", + values = ["bar", "baz"], ) -cc_flag_group( - name = "complex_flag_group", - # API TBD +# Expands to CcVariableInfo(values = ["x86_64-unknown-linux-gnu"]) +custom_variable_rule( + name = "triple", + ... ) -cc_flag_set( + +# Taken from https://bazel.build/rules/lib/builtins/Args#add +cc_add_args( + name = "single", + arg_name = "--platform", + value = ":triple", # Either a single file or a cc_variable + format = "%s", +) + +# Taken from https://bazel.build/rules/lib/builtins/Args#add_all +cc_add_args_all( + name = "multiple", + arg_name = "--foo", + values = [":file", ":file_set"], # Either files or cc_variable. + # map_each not supported. Write a custom rule if you want that. + format_each = "%s", + before_each = "--foo", + omit_if_empty = True, + uniquify = False, + # Expand_directories not yet supported. + terminate_with = "foo", +) + +# Taken from https://bazel.build/rules/lib/builtins/Args#add_joined +cc_add_args_joined( + name = "joined", + arg_name = "--foo", + values = [":file", ":file_set"], # Either files or cc_variable. + join_with = ",", + # map_each not supported. Write a custom rule if you want that. + format_each = "%s", + format_joined = "--foo=%s", + omit_if_empty = True, + uniquify = False, + # Expand_directories not yet supported. +) + +cc_args( name = "complex", actions = ["@rules_cc//actions:c_compile"], - flag_groups = [":complex_flag_group"], + add = [":single", ":multiple", ":joined"], ) -cc_flag_set_list( +cc_args_list( name = "all_flags", - flag_sets = [":simple", ":complex"], + args = [":inline", ":complex"], ) ``` ## Step 4: Define some features -A feature is a set of flags and configurations that can be enabled or disabled. +A feature is a set of args and configurations that can be enabled or disabled. Although the existing toolchain recommends using features to avoid duplication of definitions, we recommend avoiding using features unless you want the user to be able to enable / disable the feature themselves. This is because we provide -alternatives such as `cc_flag_set_list` to allow combining flag sets and +alternatives such as `cc_args_list` to allow combining arguments and specifying them on each action in the action config. ``` cc_feature( name = "my_feature", feature_name = "my_feature", - flag_sets = [":all_flags"], + args = [":all_args"], implies = [":other_feature"], ) ``` @@ -143,7 +195,7 @@ The `cc_toolchain` macro: cc_toolchain( name = "toolchain", features = [":my_feature"] - unconditional_flag_sets = [":all_warnings"], + unconditional_args = [":all_warnings"], action_configs = [":c_compile"], additional_files = [":all_action_files"], ) @@ -187,7 +239,7 @@ def cc_legacy_features(name, features): # Build file -FOO = feature(name = "foo", flag_sets=[flag_group(...)]) +FOO = feature(name = "foo", args=[arg_group(...)]) FEATURES = [FOO] cc_legacy_features(name = "legacy_features", features = FEATURES) @@ -240,7 +292,7 @@ Feature requirements can come in two formats. For example: * Features can require some subset of features to be enabled. -* Flag sets can require some subset of features to be enabled, but others to be +* Arguments can require some subset of features to be enabled, but others to be disabled. This is very confusing for toolchain authors, so we will simplify things with @@ -265,7 +317,7 @@ cc_feature_constraint( none_of = [":my_other_feature"], ) -cc_flag_set( +cc_args( name = "foo", # All of these provide with_feature. requires_any_of = [":my_feature", ":my_feature_set", ":my_feature_constraint"] diff --git a/cc/toolchains/actions.bzl b/cc/toolchains/actions.bzl index a48e120..7799a6e 100644 --- a/cc/toolchains/actions.bzl +++ b/cc/toolchains/actions.bzl @@ -13,6 +13,7 @@ # limitations under the License. """Rules to turn action types into bazel labels.""" +load("//cc/toolchains/impl:collect.bzl", "collect_action_types") load(":cc_toolchain_info.bzl", "ActionTypeInfo", "ActionTypeSetInfo") visibility("public") @@ -51,10 +52,7 @@ cc_action_type( def _cc_action_type_set_impl(ctx): return [ActionTypeSetInfo( label = ctx.label, - actions = depset(transitive = [ - attr[ActionTypeSetInfo].actions - for attr in ctx.attr.actions - ]), + actions = collect_action_types(ctx.attr.actions), )] cc_action_type_set = rule( diff --git a/cc/toolchains/args.bzl b/cc/toolchains/args.bzl new file mode 100644 index 0000000..e8c1c2b --- /dev/null +++ b/cc/toolchains/args.bzl @@ -0,0 +1,105 @@ +# Copyright 2024 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""All providers for rule-based bazel toolchain config.""" + +load( + "//cc/toolchains/impl:collect.bzl", + "collect_action_types", + "collect_files", + "collect_provider", +) +load( + ":cc_toolchain_info.bzl", + "ActionTypeSetInfo", + "AddArgsInfo", + "ArgsInfo", + "ArgsListInfo", + "FeatureConstraintInfo", +) + +visibility("public") + +def _cc_args_impl(ctx): + add_args = [AddArgsInfo( + label = ctx.label, + args = tuple(ctx.attr.args), + files = depset([]), + )] + + actions = collect_action_types(ctx.attr.actions) + files = collect_files(ctx.attr.additional_files) + requires = collect_provider(ctx.attr.requires_any_of, FeatureConstraintInfo) + + args = ArgsInfo( + label = ctx.label, + actions = actions, + requires_any_of = tuple(requires), + files = files, + args = add_args, + env = ctx.attr.env, + ) + return [ + args, + ArgsListInfo(label = ctx.label, args = tuple([args])), + ] + +cc_args = rule( + implementation = _cc_args_impl, + attrs = { + "actions": attr.label_list( + providers = [ActionTypeSetInfo], + mandatory = True, + doc = """A list of action types that this flag set applies to. + +See @rules_cc//cc/toolchains/actions:all for valid options. +""", + ), + "additional_files": attr.label_list( + allow_files = True, + doc = """Files required to add this argument to the command-line. + +For example, a flag that sets the header directory might add the headers in that +directory as additional files. +""", + ), + "args": attr.string_list( + doc = """Arguments that should be added to the command-line. + +These are evaluated in order, with earlier args appearing earlier in the +invocation of the underlying tool. +""", + ), + "env": attr.string_dict( + doc = "Environment variables to be added to the command-line.", + ), + "requires_any_of": attr.label_list( + providers = [FeatureConstraintInfo], + doc = """This will be enabled when any of the constraints are met. + +If omitted, this flag set will be enabled unconditionally. +""", + ), + }, + provides = [ArgsInfo], + doc = """Declares a list of arguments bound to a set of actions. + +Roughly equivalent to ctx.actions.args() + +Examples: + cc_args( + name = "warnings_as_errors", + args = ["-Werror"], + ) +""", +) diff --git a/cc/toolchains/cc_toolchain_info.bzl b/cc/toolchains/cc_toolchain_info.bzl index 3f1b9e6..f7a69ba 100644 --- a/cc/toolchains/cc_toolchain_info.bzl +++ b/cc/toolchains/cc_toolchain_info.bzl @@ -19,7 +19,7 @@ # decide to just require users to use the public user-facing rules. visibility([ "//cc/toolchains/...", - "//tests/...", + "//tests/rule_based_toolchain/...", ]) # Note that throughout this file, we never use a list. This is because mutable @@ -29,6 +29,7 @@ visibility([ ActionTypeInfo = provider( doc = "A type of action (eg. c-compile, c++-link-executable)", + # @unsorted-dict-items fields = { "label": "(Label) The label defining this provider. Place in error messages to simplify debugging", "name": "(str) The action name, as defined by action_names.bzl", @@ -44,23 +45,34 @@ ActionTypeSetInfo = provider( }, ) -FlagGroupInfo = provider( - doc = "A group of flags", +AddArgsInfo = provider( + doc = "A provider representation of Args.add/add_all/add_joined parameters", # @unsorted-dict-items fields = { "label": "(Label) The label defining this provider. Place in error messages to simplify debugging", - "flags": "(Sequence[str]) A list of flags to add to the command-line", + "args": "(Sequence[str]) The command-line arguments to add", + "files": "(depset[File]) The files required to use this variable", }, ) -FlagSetInfo = provider( - doc = "A set of flags to be expanded in the command line for specific actions", +ArgsInfo = provider( + doc = "A set of arguments to be added to the command line for specific actions", # @unsorted-dict-items fields = { "label": "(Label) The label defining this provider. Place in error messages to simplify debugging", "actions": "(depset[ActionTypeInfo]) The set of actions this is associated with", "requires_any_of": "(Sequence[FeatureConstraintInfo]) This will be enabled if any of the listed predicates are met. Equivalent to with_features", - "flag_groups": "(Sequence[FlagGroupInfo]) Set of flag groups to include.", + "args": "(Sequence[AddArgsInfo]) The command-line arguments to add.", + "files": "(depset[File]) Files required for the args", + "env": "(dict[str, str]) Environment variables to apply", + }, +) +ArgsListInfo = provider( + doc = "A ordered list of arguments", + # @unsorted-dict-items + fields = { + "label": "(Label) The label defining this provider. Place in error messages to simplify debugging", + "args": "(Sequence[ArgsInfo]) The flag sets contained within", }, ) @@ -71,7 +83,7 @@ FeatureInfo = provider( "label": "(Label) The label defining this provider. Place in error messages to simplify debugging", "name": "(str) The name of the feature", "enabled": "(bool) Whether this feature is enabled by default", - "flag_sets": "(depset[FlagSetInfo]) Flag sets enabled by this feature", + "args": "(Sequence[ArgsInfo]) Flag sets enabled by this feature", "implies": "(depset[FeatureInfo]) Set of features implied by this feature", "requires_any_of": "(Sequence[FeatureSetInfo]) A list of feature sets, at least one of which is required to enable this feature. This is semantically equivalent to the requires attribute of rules_cc's FeatureInfo", "provides": "(Sequence[MutuallyExclusiveCategoryInfo]) Indicates that this feature is one of several mutually exclusive alternate features.", @@ -100,6 +112,7 @@ FeatureConstraintInfo = provider( MutuallyExclusiveCategoryInfo = provider( doc = "Multiple features with the category will be mutally exclusive", + # @unsorted-dict-items fields = { "label": "(Label) The label defining this provider. Place in error messages to simplify debugging", "name": "(str) The name of the category", @@ -126,7 +139,7 @@ ActionConfigInfo = provider( "action": "(ActionTypeInfo) The name of the action", "enabled": "(bool) If True, this action is enabled unless a rule type explicitly marks it as unsupported", "tools": "(Sequence[ToolInfo]) The tool applied to the action will be the first tool in the sequence with a feature set that matches the feature configuration", - "flag_sets": "(Sequence[FlagSetInfo]) Set of flag sets the action sets", + "args": "(Sequence[ArgsInfo]) Set of flag sets the action sets", "implies": "(depset[FeatureInfo]) Set of features implied by this action config", "files": "(depset[File]) The files required to run these actions", }, diff --git a/cc/toolchains/impl/BUILD b/cc/toolchains/impl/BUILD new file mode 100644 index 0000000..8484e1c --- /dev/null +++ b/cc/toolchains/impl/BUILD @@ -0,0 +1,6 @@ +# This directory contains implementations of starlark functions that contain +# complex logic. The objective is to keep the rules themselves as simple as +# possible, so that we can perform very thorough testing on the implementation. + +# I wanted to call it private / internal, but then buildifier complains about +# referencing it from the tests directory. diff --git a/cc/toolchains/impl/collect.bzl b/cc/toolchains/impl/collect.bzl new file mode 100644 index 0000000..77979b4 --- /dev/null +++ b/cc/toolchains/impl/collect.bzl @@ -0,0 +1,55 @@ +# Copyright 2024 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Helper functions to allow us to collect data from attr.label_list.""" + +load( + "//cc/toolchains:cc_toolchain_info.bzl", + "ActionTypeSetInfo", +) + +visibility("//cc/toolchains/...") + +def collect_provider(targets, provider): + """Collects providers from a label list. + + Args: + targets: (list[Target]) An attribute from attr.label_list + provider: (provider) The provider to look up + Returns: + A list of the providers + """ + return [target[provider] for target in targets] + +def collect_defaultinfo(targets): + """Collects DefaultInfo from a label list. + + Args: + targets: (list[Target]) An attribute from attr.label_list + Returns: + A list of the associated defaultinfo + """ + return collect_provider(targets, DefaultInfo) + +def _make_collector(provider, field): + def collector(targets, direct = [], transitive = []): + # Avoid mutating what was passed in. + transitive = transitive[:] + for value in collect_provider(targets, provider): + transitive.append(getattr(value, field)) + return depset(direct = direct, transitive = transitive) + + return collector + +collect_action_types = _make_collector(ActionTypeSetInfo, "actions") +collect_files = _make_collector(DefaultInfo, "files") diff --git a/tests/rule_based_toolchain/args/BUILD b/tests/rule_based_toolchain/args/BUILD new file mode 100644 index 0000000..6806aff --- /dev/null +++ b/tests/rule_based_toolchain/args/BUILD @@ -0,0 +1,25 @@ +load("@rules_testing//lib:util.bzl", "util") +load("//cc/toolchains:args.bzl", "cc_args") +load("//tests/rule_based_toolchain:analysis_test_suite.bzl", "analysis_test_suite") +load(":args_test.bzl", "TARGETS", "TESTS") + +util.helper_target( + cc_args, + name = "simple", + actions = ["//tests/rule_based_toolchain/actions:all_compile"], + additional_files = [ + "//tests/rule_based_toolchain/testdata:file1", + "//tests/rule_based_toolchain/testdata:multiple", + ], + args = [ + "--foo", + "foo", + ], + env = {"BAR": "bar"}, +) + +analysis_test_suite( + name = "test_suite", + targets = TARGETS, + tests = TESTS, +) diff --git a/tests/rule_based_toolchain/args/args_test.bzl b/tests/rule_based_toolchain/args/args_test.bzl new file mode 100644 index 0000000..6aab2e2 --- /dev/null +++ b/tests/rule_based_toolchain/args/args_test.bzl @@ -0,0 +1,45 @@ +# Copyright 2024 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the cc_args rule.""" + +load( + "//cc/toolchains:cc_toolchain_info.bzl", + "ArgsInfo", +) + +visibility("//tests/rule_based_toolchain/...") + +def _test_simple_args_impl(env, targets): + simple = env.expect.that_target(targets.simple).provider(ArgsInfo) + simple.actions().contains_exactly([ + targets.c_compile.label, + targets.cpp_compile.label, + ]) + simple.args().contains_exactly([targets.simple.label]) + simple.env().contains_exactly({"BAR": "bar"}) + simple.files().contains_exactly([ + "tests/rule_based_toolchain/testdata/file1", + "tests/rule_based_toolchain/testdata/multiple1", + "tests/rule_based_toolchain/testdata/multiple2", + ]) + +TARGETS = [ + ":simple", + "//tests/rule_based_toolchain/actions:c_compile", + "//tests/rule_based_toolchain/actions:cpp_compile", +] + +TESTS = { + "simple_test": _test_simple_args_impl, +} diff --git a/tests/rule_based_toolchain/subjects.bzl b/tests/rule_based_toolchain/subjects.bzl index b330093..53daeb5 100644 --- a/tests/rule_based_toolchain/subjects.bzl +++ b/tests/rule_based_toolchain/subjects.bzl @@ -20,11 +20,11 @@ load( "ActionConfigSetInfo", "ActionTypeInfo", "ActionTypeSetInfo", + "AddArgsInfo", + "ArgsInfo", "FeatureConstraintInfo", "FeatureInfo", "FeatureSetInfo", - "FlagGroupInfo", - "FlagSetInfo", "MutuallyExclusiveCategoryInfo", "ToolInfo", ) @@ -94,22 +94,24 @@ _FeatureConstraintFactory = generate_factory( ) # buildifier: disable=name-conventions -_FlagGroupFactory = generate_factory( - FlagGroupInfo, - "FlagGroupInfo", +_AddArgsFactory = generate_factory( + AddArgsInfo, + "AddArgsInfo", dict( - flags = subjects.collection, + args = subjects.collection, + files = subjects.depset_file, ), ) # buildifier: disable=name-conventions -_FlagSetFactory = generate_factory( - FlagSetInfo, - "FlagSetInfo", +_ArgsFactory = generate_factory( + ArgsInfo, + "ArgsInfo", dict( actions = ProviderDepset(_ActionTypeFactory), - # Use a collection here because we don't want to - flag_groups = subjects.collection, + args = ProviderSequence(_AddArgsFactory), + env = subjects.dict, + files = subjects.depset_file, requires_any_of = ProviderSequence(_FeatureConstraintFactory), ), ) @@ -144,7 +146,7 @@ _ActionConfigFactory = generate_factory( action = _ActionTypeFactory, enabled = subjects.bool, tools = ProviderSequence(_ToolFactory), - flag_sets = ProviderSequence(_FlagSetFactory), + flag_sets = ProviderSequence(_ArgsFactory), implies = ProviderDepset(_FeatureFactory), files = subjects.depset_file, ), @@ -174,8 +176,8 @@ _ActionConfigSetFactory = struct( FACTORIES = [ _ActionTypeFactory, _ActionTypeSetFactory, - _FlagGroupFactory, - _FlagSetFactory, + _AddArgsFactory, + _ArgsFactory, _MutuallyExclusiveCategoryFactory, _FeatureFactory, _FeatureConstraintFactory, diff --git a/tests/rule_based_toolchain/testdata/BUILD b/tests/rule_based_toolchain/testdata/BUILD new file mode 100644 index 0000000..6007720 --- /dev/null +++ b/tests/rule_based_toolchain/testdata/BUILD @@ -0,0 +1,16 @@ +package(default_visibility = ["//tests/rule_based_toolchain:__subpackages__"]) + +exports_files( + glob( + ["*"], + exclude = ["BUILD"], + ), +) + +filegroup( + name = "multiple", + srcs = [ + "multiple1", + "multiple2", + ], +) diff --git a/tests/rule_based_toolchain/testdata/file1 b/tests/rule_based_toolchain/testdata/file1 new file mode 100644 index 0000000..e69de29 diff --git a/tests/rule_based_toolchain/testdata/file2 b/tests/rule_based_toolchain/testdata/file2 new file mode 100644 index 0000000..e69de29 diff --git a/tests/rule_based_toolchain/testdata/multiple1 b/tests/rule_based_toolchain/testdata/multiple1 new file mode 100644 index 0000000..e69de29 diff --git a/tests/rule_based_toolchain/testdata/multiple2 b/tests/rule_based_toolchain/testdata/multiple2 new file mode 100644 index 0000000..e69de29