Add a script for converting CROSSTOOL files to Starlark rules

Progress towards issue #5380

RELNOTES: None
PiperOrigin-RevId: 230795058
diff --git a/WORKSPACE b/WORKSPACE
index 5f8401c..f6c948f 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -52,3 +52,16 @@
         """echo 'py_library(name = "mock", srcs = ["__init__.py"], visibility = ["//visibility:public"],)' > py/mock/BUILD""",
     ],
 )
+
+# Go rules and proto support
+http_archive(
+    name = "io_bazel_rules_go",
+    sha256 = "8be57ff66da79d9e4bd434c860dce589195b9101b2c187d144014bbca23b5166",
+    strip_prefix = "rules_go-0.16.3",
+    urls = [
+        "https://github.com/bazelbuild/rules_go/archive/0.16.3.tar.gz",
+    ],
+)
+load("@io_bazel_rules_go//go:def.bzl", "go_rules_dependencies", "go_register_toolchains")
+go_rules_dependencies()
+go_register_toolchains()
diff --git a/third_party/com/github/bazelbuild/bazel/src/main/protobuf/BUILD b/third_party/com/github/bazelbuild/bazel/src/main/protobuf/BUILD
index 6fafeb8..b96bbb9 100644
--- a/third_party/com/github/bazelbuild/bazel/src/main/protobuf/BUILD
+++ b/third_party/com/github/bazelbuild/bazel/src/main/protobuf/BUILD
@@ -1,6 +1,7 @@
 licenses(["notice"])  # Apache 2.0
 
 load("@com_google_protobuf//:protobuf.bzl", "py_proto_library")
+load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
 
 py_proto_library(
     name = "crosstool_config_py_pb2",
@@ -9,3 +10,20 @@
         "//tools/migration:__pkg__",
     ],
 )
+
+proto_library(
+    name = "crosstool_config_pb2",
+    srcs = ["crosstool_config.proto"],
+    visibility = [
+        "//tools/migration:__pkg__",
+    ],
+)
+
+go_proto_library(
+    name = "crosstool_config_go_proto",
+    importpath = "third_party/com/github/bazelbuild/bazel/src/main/protobuf/crosstool_config_go_proto",
+    proto = ":crosstool_config_pb2",
+    visibility = [
+        "//tools/migration:__pkg__",
+    ],
+)
diff --git a/tools/migration/BUILD b/tools/migration/BUILD
index 404b97d..fbc2ed1 100644
--- a/tools/migration/BUILD
+++ b/tools/migration/BUILD
@@ -14,6 +14,9 @@
 
 package(default_visibility = ["//visibility:public"])
 
+# Go rules
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test")
+
 licenses(["notice"])  # Apache 2.0
 
 py_binary(
@@ -82,3 +85,31 @@
         "@py_mock//py/mock",
     ],
 )
+
+go_binary(
+    name = "convert_crosstool_to_starlark",
+    srcs = ["convert_crosstool_to_starlark.go"],
+    deps = [
+        ":crosstooltostarlarklib",
+        "//third_party/com/github/bazelbuild/bazel/src/main/protobuf:crosstool_config_go_proto",
+        "@com_github_golang_protobuf//proto:go_default_library",
+    ],
+)
+
+go_library(
+    name = "crosstooltostarlarklib",
+    srcs = ["crosstool_to_starlark_lib.go"],
+    importpath = "tools/migration/crosstooltostarlarklib",
+    deps = ["//third_party/com/github/bazelbuild/bazel/src/main/protobuf:crosstool_config_go_proto"],
+)
+
+go_test(
+    name = "crosstooltostarlarklib_test",
+    size = "small",
+    srcs = ["crosstool_to_starlark_lib_test.go"],
+    embed = [":crosstooltostarlarklib"],
+    deps = [
+        "//third_party/com/github/bazelbuild/bazel/src/main/protobuf:crosstool_config_go_proto",
+        "@com_github_golang_protobuf//proto:go_default_library",
+    ],
+)
diff --git a/tools/migration/convert_crosstool_to_starlark.go b/tools/migration/convert_crosstool_to_starlark.go
new file mode 100644
index 0000000..ddd64ca
--- /dev/null
+++ b/tools/migration/convert_crosstool_to_starlark.go
@@ -0,0 +1,68 @@
+/*
+The convert_crosstool_to_starlark script takes in a CROSSTOOL file and
+generates a Starlark rule.
+
+See https://github.com/bazelbuild/bazel/issues/5380
+
+Example usage:
+bazel run \
+@rules_cc//tools/migration:convert_crosstool_to_starlark -- \
+--crosstool=/path/to/CROSSTOOL \
+--output_location=/path/to/cc_config.bzl
+*/
+package main
+
+import (
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"os"
+
+	"log"
+	"github.com/golang/protobuf/proto"
+	crosstoolpb "third_party/com/github/bazelbuild/bazel/src/main/protobuf/crosstool_config_go_proto"
+
+	"tools/migration/crosstooltostarlarklib"
+)
+
+var (
+	crosstoolLocation = flag.String(
+		"crosstool", "", "Location of the CROSSTOOL file")
+	outputLocation = flag.String(
+		"output_location", "", "Location of the output .bzl file")
+)
+
+func main() {
+
+	if *crosstoolLocation == "" {
+		log.Fatalf("Missing mandatory argument 'crosstool'")
+	}
+	if *outputLocation == "" {
+		log.Fatalf("Missing mandatory argument 'output_location'")
+	}
+
+	in, err := ioutil.ReadFile(*crosstoolLocation)
+	if err != nil {
+		log.Fatalf("Error reading CROSSTOOL file:", err)
+	}
+	crosstool := &crosstoolpb.CrosstoolRelease{}
+	if err := proto.UnmarshalText(string(in), crosstool); err != nil {
+		log.Fatalf("Failed to parse CROSSTOOL:", err)
+	}
+
+	file, err := os.Create(*outputLocation)
+	if err != nil {
+		log.Fatalf("Error creating output file:", err)
+	}
+	defer file.Close()
+
+	rule, err := crosstooltostarlarklib.Transform(crosstool)
+	if err != nil {
+		log.Fatalf("Error converting CROSSTOOL to a Starlark rule:", err)
+	}
+
+	if _, err := file.WriteString(rule); err != nil {
+		log.Fatalf("Error converting CROSSTOOL to a Starlark rule:", err)
+	}
+	fmt.Println("Success!")
+}
diff --git a/tools/migration/crosstool_to_starlark_lib.go b/tools/migration/crosstool_to_starlark_lib.go
new file mode 100644
index 0000000..a3e230c
--- /dev/null
+++ b/tools/migration/crosstool_to_starlark_lib.go
@@ -0,0 +1,1355 @@
+/*
+Package crosstooltostarlarklib provides the Transform method
+for conversion of a CROSSTOOL file to a Starlark rule.
+
+https://github.com/bazelbuild/bazel/issues/5380
+*/
+package crosstooltostarlarklib
+
+import (
+	"bytes"
+	"fmt"
+	"sort"
+	"strings"
+
+	crosstoolpb "third_party/com/github/bazelbuild/bazel/src/main/protobuf/crosstool_config_go_proto"
+)
+
+// CToolchainIdentifier is what we'll use to differ between CToolchains
+// If a CToolchain can be distinguished from the other CToolchains
+// by only one of the fields (eg if cpu is different for each CToolchain
+// then only that field will be set.
+type CToolchainIdentifier struct {
+	cpu      string
+	compiler string
+}
+
+// Writes the load statement for the cc_toolchain_config_lib
+func getCcToolchainConfigHeader() string {
+	return `load("@bazel_tools//tools/cpp:cc_toolchain_config_lib.bzl",
+    "action_config",
+    "artifact_name_pattern",
+    "env_entry",
+    "env_set",
+    "feature",
+    "feature_set",
+    "flag_group",
+    "flag_set",
+    "make_variable",
+    "tool",
+    "tool_path",
+    "variable_with_value",
+    "with_feature_set",
+)
+`
+}
+
+var allCompileActions = []string{
+	"c-compile",
+	"c++-compile",
+	"linkstamp-compile",
+	"assemble",
+	"preprocess-assemble",
+	"c++-header-parsing",
+	"c++-module-compile",
+	"c++-module-codegen",
+	"clif-match",
+	"lto-backend",
+}
+
+var allCppCompileActions = []string{
+	"c++-compile",
+	"linkstamp-compile",
+	"c++-header-parsing",
+	"c++-module-compile",
+	"c++-module-codegen",
+	"clif-match",
+}
+
+var preprocessorCompileActions = []string{
+	"c-compile",
+	"c++-compile",
+	"linkstamp-compile",
+	"preprocess-assemble",
+	"c++-header-parsing",
+	"c++-module-compile",
+	"clif-match",
+}
+
+var codegenCompileActions = []string{
+	"c-compile",
+	"c++-compile",
+	"linkstamp-compile",
+	"assemble",
+	"preprocess-assemble",
+	"c++-module-codegen",
+	"lto-backend",
+}
+
+var allLinkActions = []string{
+	"c++-link-executable",
+	"c++-link-dynamic-library",
+	"c++-link-nodeps-dynamic-library",
+}
+
+var actionNames = map[string]string{
+	"c-compile":                       "ACTION_NAMES.c_compile",
+	"c++-compile":                     "ACTION_NAMES.cpp_compile",
+	"linkstamp-compile":               "ACTION_NAMES.linkstamp_compile",
+	"cc-flags-make-variable":          "ACTION_NAMES.cc_flags_make_variable",
+	"c++-module-codegen":              "ACTION_NAMES.cpp_module_codegen",
+	"c++-header-parsing":              "ACTION_NAMES.cpp_header_parsing",
+	"c++-module-compile":              "ACTION_NAMES.cpp_module_compile",
+	"assemble":                        "ACTION_NAMES.assemble",
+	"preprocess-assemble":             "ACTION_NAMES.preprocess_assemble",
+	"lto-indexing":                    "ACTION_NAMES.lto_indexing",
+	"lto-backend":                     "ACTION_NAMES.lto_backend",
+	"c++-link-executable":             "ACTION_NAMES.cpp_link_executable",
+	"c++-link-dynamic-library":        "ACTION_NAMES.cpp_link_dynamic_library",
+	"c++-link-nodeps-dynamic-library": "ACTION_NAMES.cpp_link_nodeps_dynamic_library",
+	"c++-link-static-library":         "ACTION_NAMES.cpp_link_static_library",
+	"strip":                           "ACTION_NAMES.strip",
+	"objc-compile":                    "ACTION_NAMES.objc_compile",
+	"objc++-compile":                  "ACTION_NAMES.objcpp_compile",
+	"clif-match":                      "ACTION_NAMES.clif_match",
+	"objcopy_embed_data":              "ACTION_NAMES.objcopy_embed_data",
+	"ld_embed_data":                   "ACTION_NAMES.ld_embed_data",
+}
+
+func getLoadActionsStmt() string {
+	return "load(\"@bazel_tools//tools/build_defs/cc:action_names.bzl\", \"ACTION_NAMES\")\n\n"
+}
+
+// Returns a map {toolchain_identifier : CToolchainIdentifier}
+func toolchainToCToolchainIdentifier(
+	crosstool *crosstoolpb.CrosstoolRelease) map[string]CToolchainIdentifier {
+	cpuToCompiler := make(map[string][]string)
+	compilerToCPU := make(map[string][]string)
+	var cpus []string
+	var compilers []string
+	var identifiers []string
+	res := make(map[string]CToolchainIdentifier)
+	for _, cToolchain := range crosstool.GetToolchain() {
+		cpu := cToolchain.GetTargetCpu()
+		compiler := cToolchain.GetCompiler()
+
+		cpuToCompiler[cpu] = append(cpuToCompiler[cpu], compiler)
+		compilerToCPU[compiler] = append(compilerToCPU[compiler], cpu)
+
+		cpus = append(cpus, cToolchain.GetTargetCpu())
+		compilers = append(compilers, cToolchain.GetCompiler())
+		identifiers = append(identifiers, cToolchain.GetToolchainIdentifier())
+	}
+
+	for i := range cpus {
+		if len(cpuToCompiler[cpus[i]]) == 1 {
+			// if cpu is unique among CToolchains, we don't need the compiler field
+			res[identifiers[i]] = CToolchainIdentifier{cpu: cpus[i], compiler: ""}
+		} else {
+			res[identifiers[i]] = CToolchainIdentifier{
+				cpu:      cpus[i],
+				compiler: compilers[i],
+			}
+		}
+	}
+	return res
+}
+
+func getConditionStatementForCToolchainIdentifier(identifier CToolchainIdentifier) string {
+	if identifier.compiler != "" {
+		return fmt.Sprintf(
+			"ctx.attr.cpu == \"%s\" and ctx.attr.compiler == \"%s\"",
+			identifier.cpu,
+			identifier.compiler)
+	}
+	return fmt.Sprintf("ctx.attr.cpu == \"%s\"", identifier.cpu)
+}
+
+func isArrayPrefix(prefix []string, arr []string) bool {
+	if len(prefix) > len(arr) {
+		return false
+	}
+	for i := 0; i < len(prefix); i++ {
+		if arr[i] != prefix[i] {
+			return false
+		}
+	}
+	return true
+}
+
+func isAllCompileActions(actions []string) (bool, []string) {
+	if isArrayPrefix(allCompileActions, actions) {
+		return true, actions[len(allCompileActions):]
+	}
+	return false, actions
+}
+
+func isAllCppCompileActions(actions []string) (bool, []string) {
+	if isArrayPrefix(allCppCompileActions, actions) {
+		return true, actions[len(allCppCompileActions):]
+	}
+	return false, actions
+}
+
+func isPreprocessorCompileActions(actions []string) (bool, []string) {
+	if isArrayPrefix(preprocessorCompileActions, actions) {
+		return true, actions[len(preprocessorCompileActions):]
+	}
+	return false, actions
+}
+
+func isCodegenCompileActions(actions []string) (bool, []string) {
+	if isArrayPrefix(codegenCompileActions, actions) {
+		return true, actions[len(codegenCompileActions):]
+	}
+	return false, actions
+}
+
+func isAllLinkActions(actions []string) (bool, []string) {
+	if isArrayPrefix(allLinkActions, actions) {
+		return true, actions[len(allLinkActions):]
+	}
+	return false, actions
+}
+
+func getActionNames(actions []string) []string {
+	var res []string
+	for _, el := range actions {
+		if name, ok := actionNames[el]; ok {
+			res = append(res, name)
+		} else {
+			res = append(res, el)
+		}
+	}
+	return res
+}
+
+func getListOfActions(name string, depth int) string {
+	var res []string
+	if name == "all_compile_actions" {
+		res = getActionNames(allCompileActions)
+	} else if name == "all_cpp_compile_actions" {
+		res = getActionNames(allCppCompileActions)
+	} else if name == "preprocessor_compile_actions" {
+		res = getActionNames(preprocessorCompileActions)
+	} else if name == "codegen_compile_actions" {
+		res = getActionNames(codegenCompileActions)
+	} else if name == "all_link_actions" {
+		res = getActionNames(allLinkActions)
+	}
+	stmt := fmt.Sprintf("%s%s = %s\n\n", getTabs(depth),
+		name, makeStringArr(res, depth /* isPlainString= */, false))
+	return stmt
+}
+
+func processActions(actions []string, depth int) []string {
+	var res []string
+	var ok bool
+	initLen := len(actions)
+	if ok, actions = isAllCompileActions(actions); ok {
+		res = append(res, "all_compile_actions")
+	}
+	if ok, actions = isAllCppCompileActions(actions); ok {
+		res = append(res, "all_cpp_compile_actions")
+	}
+	if ok, actions = isPreprocessorCompileActions(actions); ok {
+		res = append(res, "preprocessor_compile_actions")
+	}
+	if ok, actions = isCodegenCompileActions(actions); ok {
+		res = append(res, "codegen_actions")
+	}
+	if ok, actions = isAllLinkActions(actions); ok {
+		res = append(res, "all_link_actions")
+	}
+	if len(actions) != 0 {
+		actions = getActionNames(actions)
+		newDepth := depth + 1
+		if len(actions) != initLen {
+			newDepth++
+		}
+		res = append(res, makeStringArr(actions, newDepth /* isPlainString= */, false))
+	}
+	return res
+}
+
+func getRule(cToolchainIdentifiers map[string]CToolchainIdentifier) string {
+	cpus := make(map[string]bool)
+	compilers := make(map[string]bool)
+	for _, val := range cToolchainIdentifiers {
+		cpus[val.cpu] = true
+		if val.compiler != "" {
+			compilers[val.compiler] = true
+		}
+	}
+
+	var cpuValues []string
+	for cpu := range cpus {
+		cpuValues = append(cpuValues, cpu)
+	}
+
+	var compilerValues []string
+	for compiler := range compilers {
+		compilerValues = append(compilerValues, compiler)
+	}
+	var args []string
+	sort.Strings(cpuValues)
+	args = append(args,
+		fmt.Sprintf(
+			`"cpu": attr.string(mandatory=True, values=["%s"]),`,
+			strings.Join(cpuValues, "\", \"")))
+	if len(compilerValues) != 0 {
+		sort.Strings(compilerValues)
+		args = append(args,
+			fmt.Sprintf(`"compiler": attr.string(values=["%s"]),`,
+				strings.Join(compilerValues, "\", \"")))
+	}
+	return fmt.Sprintf(`cc_toolchain_config_rule =  rule(
+    implementation = _impl,
+    attrs = {
+        %s
+    },
+    provides = [CcToolchainConfigInfo],
+    executable = True,
+)
+`, strings.Join(args, "\n        "))
+}
+
+func getImplHeader() string {
+	return "def _impl(ctx):\n"
+}
+
+func getStringStatement(crosstool *crosstoolpb.CrosstoolRelease,
+	cToolchainIdentifiers map[string]CToolchainIdentifier, field string,
+	depth int) string {
+
+	identifiers := getToolchainIdentifiers(crosstool)
+	var fieldValues []string
+	if field == "toolchain_identifier" {
+		fieldValues = getToolchainIdentifiers(crosstool)
+	} else if field == "host_system_name" {
+		fieldValues = getHostSystemNames(crosstool)
+	} else if field == "target_system_name" {
+		fieldValues = getTargetSystemNames(crosstool)
+	} else if field == "target_cpu" {
+		fieldValues = getTargetCpus(crosstool)
+	} else if field == "target_libc" {
+		fieldValues = getTargetLibcs(crosstool)
+	} else if field == "compiler" {
+		fieldValues = getCompilers(crosstool)
+	} else if field == "abi_version" {
+		fieldValues = getAbiVersions(crosstool)
+	} else if field == "abi_libc_version" {
+		fieldValues = getAbiLibcVersions(crosstool)
+	} else if field == "cc_target_os" {
+		fieldValues = getCcTargetOss(crosstool)
+	} else if field == "builtin_sysroot" {
+		fieldValues = getBuiltinSysroots(crosstool)
+	}
+
+	mappedValuesToIds := getMappedStringValuesToIdentifiers(identifiers, fieldValues)
+	return getAssignmentStatement(field, mappedValuesToIds, crosstool,
+		cToolchainIdentifiers, depth /* isPlainString= */, true)
+}
+
+func getFeatures(crosstool *crosstoolpb.CrosstoolRelease) (
+	map[string][]string, map[string]map[string][]string) {
+	featureNameToFeature := make(map[string]map[string][]string)
+	toolchainToFeatures := make(map[string][]string)
+	for _, toolchain := range crosstool.GetToolchain() {
+		id := toolchain.GetToolchainIdentifier()
+		if len(toolchain.GetFeature()) == 0 {
+			toolchainToFeatures[id] = []string{}
+		}
+		for _, feature := range toolchain.GetFeature() {
+			featureName := strings.ToLower(feature.GetName()) + "_feature"
+			featureName = strings.Replace(featureName, "+", "p", -1)
+			featureName = strings.Replace(featureName, ".", "_", -1)
+			stringFeature := parseFeature(feature, 1)
+			if _, ok := featureNameToFeature[featureName]; !ok {
+				featureNameToFeature[featureName] = make(map[string][]string)
+			}
+			featureNameToFeature[featureName][stringFeature] = append(
+				featureNameToFeature[featureName][stringFeature], id)
+			toolchainToFeatures[id] = append(toolchainToFeatures[id], featureName)
+		}
+	}
+	return toolchainToFeatures, featureNameToFeature
+}
+
+func getFeaturesDeclaration(crosstool *crosstoolpb.CrosstoolRelease,
+	cToolchainIdentifiers map[string]CToolchainIdentifier,
+	featureNameToFeature map[string]map[string][]string, depth int) string {
+	var res []string
+	for featureName, featureStringToID := range featureNameToFeature {
+		res = append(res,
+			getAssignmentStatement(
+				featureName,
+				featureStringToID,
+				crosstool,
+				cToolchainIdentifiers,
+				depth,
+				/* isPlainString= */ false))
+	}
+	return strings.Join(res, "")
+}
+
+func getFeaturesStmt(cToolchainIdentifiers map[string]CToolchainIdentifier,
+	toolchainToFeatures map[string][]string, depth int) string {
+	var res []string
+	arrToIdentifier := make(map[string][]string)
+	for id, features := range toolchainToFeatures {
+		arrayString := strings.Join(features, "{arrayFieldDelimiter}")
+		arrToIdentifier[arrayString] = append(arrToIdentifier[arrayString], id)
+	}
+	res = append(res,
+		getStringArrStatement(
+			"features",
+			arrToIdentifier,
+			cToolchainIdentifiers,
+			depth,
+			/* isPlainString= */ false))
+	return strings.Join(res, "\n")
+}
+
+func getActions(crosstool *crosstoolpb.CrosstoolRelease) (
+	map[string][]string, map[string]map[string][]string) {
+	actionNameToAction := make(map[string]map[string][]string)
+	toolchainToActions := make(map[string][]string)
+	for _, toolchain := range crosstool.GetToolchain() {
+		id := toolchain.GetToolchainIdentifier()
+		var actionName string
+		if len(toolchain.GetActionConfig()) == 0 {
+			toolchainToActions[id] = []string{}
+		}
+		for _, action := range toolchain.GetActionConfig() {
+			if aName, ok := actionNames[action.GetActionName()]; ok {
+				actionName = aName
+			} else {
+				actionName = strings.ToLower(action.GetActionName())
+				actionName = strings.Replace(actionName, "+", "p", -1)
+				actionName = strings.Replace(actionName, ".", "_", -1)
+			}
+			stringAction := parseAction(action, 1)
+			if _, ok := actionNameToAction[actionName]; !ok {
+				actionNameToAction[actionName] = make(map[string][]string)
+			}
+			actionNameToAction[actionName][stringAction] = append(
+				actionNameToAction[actionName][stringAction], id)
+			toolchainToActions[id] = append(
+				toolchainToActions[id],
+				strings.TrimPrefix(strings.ToLower(actionName), "action_names.")+"_action")
+		}
+	}
+	return toolchainToActions, actionNameToAction
+}
+
+func getActionConfigsDeclaration(
+	crosstool *crosstoolpb.CrosstoolRelease,
+	cToolchainIdentifiers map[string]CToolchainIdentifier,
+	actionNameToAction map[string]map[string][]string, depth int) string {
+	var res []string
+	for actionName, actionStringToID := range actionNameToAction {
+		variableName := strings.TrimPrefix(strings.ToLower(actionName), "action_names.") + "_action"
+		res = append(res,
+			getAssignmentStatement(
+				variableName,
+				actionStringToID,
+				crosstool,
+				cToolchainIdentifiers,
+				depth,
+				/* isPlainString= */ false))
+	}
+	return strings.Join(res, "")
+}
+
+func getActionConfigsStmt(
+	cToolchainIdentifiers map[string]CToolchainIdentifier,
+	toolchainToActions map[string][]string, depth int) string {
+	var res []string
+	arrToIdentifier := make(map[string][]string)
+	for id, actions := range toolchainToActions {
+		var arrayString string
+		arrayString = strings.Join(actions, "{arrayFieldDelimiter}")
+		arrToIdentifier[arrayString] = append(arrToIdentifier[arrayString], id)
+	}
+	res = append(res,
+		getStringArrStatement(
+			"action_configs",
+			arrToIdentifier,
+			cToolchainIdentifiers,
+			depth,
+			/* isPlainString= */ false))
+	return strings.Join(res, "\n")
+}
+
+func parseAction(action *crosstoolpb.CToolchain_ActionConfig, depth int) string {
+	actionName := action.GetActionName()
+	aName := ""
+	if val, ok := actionNames[actionName]; ok {
+		aName = val
+	} else {
+		aName = "\"" + action.GetActionName() + "\""
+	}
+	name := fmt.Sprintf("action_name = %s", aName)
+	fields := []string{name}
+	if action.GetEnabled() {
+		fields = append(fields, "enabled = True")
+	}
+	if len(action.GetFlagSet()) != 0 {
+		flagSets := "flag_sets = " +
+			parseFlagSets(action.GetFlagSet(), depth+1)
+		fields = append(fields, flagSets)
+	}
+	if len(action.GetImplies()) != 0 {
+		implies := "implies = " +
+			makeStringArr(action.GetImplies(), depth+1 /* isPlainString= */, true)
+		fields = append(fields, implies)
+	}
+	if len(action.GetTool()) != 0 {
+		tools := "tools = " + parseTools(action.GetTool(), depth+1)
+		fields = append(fields, tools)
+	}
+	return createObject("action_config", fields, depth)
+}
+
+func getStringArrStatement(attr string, arrValToIds map[string][]string,
+	cToolchainIdentifiers map[string]CToolchainIdentifier, depth int, plainString bool) string {
+	var b bytes.Buffer
+	if len(arrValToIds) == 0 {
+		b.WriteString(fmt.Sprintf("%s%s = []\n", getTabs(depth), attr))
+	} else if len(arrValToIds) == 1 {
+		for value := range arrValToIds {
+			var arr []string
+			if value == "" {
+				arr = []string{}
+			} else if value == "None" {
+				b.WriteString(fmt.Sprintf("%s%s = None\n", getTabs(depth), attr))
+				break
+			} else {
+				arr = strings.Split(value, "{arrayFieldDelimiter}")
+			}
+			b.WriteString(
+				fmt.Sprintf(
+					"%s%s = %s\n",
+					getTabs(depth),
+					attr,
+					makeStringArr(arr, depth+1, plainString)))
+			break
+		}
+	} else {
+		first := true
+		var keys []string
+		for k := range arrValToIds {
+			keys = append(keys, k)
+		}
+		sort.Strings(keys)
+		for _, value := range keys {
+			ids := arrValToIds[value]
+			branch := "elif"
+			if first {
+				branch = "if"
+			}
+			first = false
+			var arr []string
+			if value == "" {
+				arr = []string{}
+			} else if value == "None" {
+				b.WriteString(
+					getIfStatement(
+						branch, ids, attr, "None", cToolchainIdentifiers,
+						depth /* isPlainString= */, true))
+				continue
+			} else {
+				arr = strings.Split(value, "{arrayFieldDelimiter}")
+			}
+			b.WriteString(
+				getIfStatement(branch, ids, attr,
+					makeStringArr(arr, depth+1, plainString),
+					cToolchainIdentifiers, depth /* isPlainString= */, false))
+		}
+		b.WriteString(fmt.Sprintf("%selse:\n%sfail(\"Unreachable\")\n", getTabs(depth), getTabs(depth+1)))
+	}
+	b.WriteString("\n")
+	return b.String()
+}
+
+func getStringArr(crosstool *crosstoolpb.CrosstoolRelease,
+	cToolchainIdentifiers map[string]CToolchainIdentifier, attr string, depth int) string {
+	var res []string
+	arrToIdentifier := make(map[string][]string)
+	for _, toolchain := range crosstool.GetToolchain() {
+		id := toolchain.GetToolchainIdentifier()
+		arrayString := strings.Join(getArrField(attr, toolchain), "{arrayFieldDelimiter}")
+		arrToIdentifier[arrayString] = append(arrToIdentifier[arrayString], id)
+	}
+	statement := getStringArrStatement(attr, arrToIdentifier, cToolchainIdentifiers, depth /* isPlainString= */, true)
+	res = append(res, statement)
+	return strings.Join(res, "\n")
+}
+
+func getArrField(attr string, toolchain *crosstoolpb.CToolchain) []string {
+	var arr []string
+	if attr == "cxx_builtin_include_directories" {
+		arr = toolchain.GetCxxBuiltinIncludeDirectory()
+	}
+	return arr
+}
+
+func getTabs(depth int) string {
+	var res string
+	for i := 0; i < depth; i++ {
+		res = res + "    "
+	}
+	return res
+}
+
+func createObject(objtype string, fields []string, depth int) string {
+	if len(fields) == 0 {
+		return objtype + "()"
+	}
+	singleLine := objtype + "(" + strings.Join(fields, ", ") + ")"
+	if len(singleLine) < 60 {
+		return singleLine
+	}
+	return objtype +
+		"(\n" +
+		getTabs(depth+1) +
+		strings.Join(fields, ",\n"+getTabs(depth+1)) +
+		",\n" + getTabs(depth) +
+		")"
+}
+
+func getArtifactNamePatterns(crosstool *crosstoolpb.CrosstoolRelease,
+	cToolchainIdentifiers map[string]CToolchainIdentifier, depth int) string {
+	var res []string
+	artifactToIds := make(map[string][]string)
+	for _, toolchain := range crosstool.GetToolchain() {
+		artifactNamePatterns := parseArtifactNamePatterns(
+			toolchain.GetArtifactNamePattern(),
+			depth)
+		artifactToIds[artifactNamePatterns] = append(
+			artifactToIds[artifactNamePatterns],
+			toolchain.GetToolchainIdentifier())
+	}
+	res = append(res,
+		getAssignmentStatement(
+			"artifact_name_patterns",
+			artifactToIds,
+			crosstool,
+			cToolchainIdentifiers,
+			depth,
+			/* isPlainString= */ false))
+	return strings.Join(res, "\n")
+}
+
+func parseArtifactNamePatterns(
+	artifactNamePatterns []*crosstoolpb.CToolchain_ArtifactNamePattern, depth int) string {
+	var res []string
+	for _, pattern := range artifactNamePatterns {
+		res = append(res, parseArtifactNamePattern(pattern, depth+1))
+	}
+	return makeStringArr(res, depth /* isPlainString= */, false)
+}
+
+func parseArtifactNamePattern(
+	artifactNamePattern *crosstoolpb.CToolchain_ArtifactNamePattern, depth int) string {
+	categoryName := fmt.Sprintf("category_name = \"%s\"", artifactNamePattern.GetCategoryName())
+	prefix := fmt.Sprintf("prefix = \"%s\"", artifactNamePattern.GetPrefix())
+	extension := fmt.Sprintf("extension = \"%s\"", artifactNamePattern.GetExtension())
+	fields := []string{categoryName, prefix, extension}
+	return createObject("artifact_name_pattern", fields, depth)
+}
+
+func parseFeature(feature *crosstoolpb.CToolchain_Feature, depth int) string {
+	name := fmt.Sprintf("name = \"%s\"", feature.GetName())
+
+	fields := []string{name}
+	if feature.GetEnabled() {
+		fields = append(fields, "enabled = True")
+	}
+
+	if len(feature.GetFlagSet()) > 0 {
+		flagSets := "flag_sets = " +
+			parseFlagSets(feature.GetFlagSet(), depth+1)
+		fields = append(fields, flagSets)
+	}
+	if len(feature.GetEnvSet()) > 0 {
+		envSets := "env_sets = " + parseEnvSets(feature.GetEnvSet(), depth+1)
+		fields = append(fields, envSets)
+	}
+	if len(feature.GetRequires()) > 0 {
+		requires := "requires = " + parseFeatureSets(feature.GetRequires(), depth+1)
+		fields = append(fields, requires)
+	}
+	if len(feature.GetImplies()) > 0 {
+		implies := "implies = " +
+			makeStringArr(feature.GetImplies(), depth+1 /* isPlainString= */, true)
+		fields = append(fields, implies)
+	}
+	if len(feature.GetProvides()) > 0 {
+		provides := "provides = " +
+			makeStringArr(feature.GetProvides(), depth+1 /* isPlainString= */, true)
+		fields = append(fields, provides)
+	}
+	return createObject("feature", fields, depth)
+}
+
+func parseFlagSets(flagSets []*crosstoolpb.CToolchain_FlagSet, depth int) string {
+	var res []string
+	for _, flagSet := range flagSets {
+		parsedFlagset := parseFlagSet(flagSet, depth+1)
+		res = append(res, parsedFlagset)
+	}
+	return makeStringArr(res, depth /* isPlainString= */, false)
+}
+
+func parseFlagSet(flagSet *crosstoolpb.CToolchain_FlagSet, depth int) string {
+	var fields []string
+	if len(flagSet.GetAction()) > 0 {
+		actionArr := processActions(flagSet.GetAction(), depth)
+		actions := "actions = " + strings.Join(actionArr, " +\n"+getTabs(depth+2))
+		fields = append(fields, actions)
+	}
+	if len(flagSet.GetFlagGroup()) > 0 {
+		flagGroups := "flag_groups = " +
+			parseFlagGroups(flagSet.GetFlagGroup(), depth+1)
+		fields = append(fields, flagGroups)
+	}
+	if len(flagSet.GetWithFeature()) > 0 {
+		withFeatures := "with_features = " +
+			parseWithFeatureSets(flagSet.GetWithFeature(), depth+1)
+		fields = append(fields, withFeatures)
+	}
+	return createObject("flag_set", fields, depth)
+}
+
+func parseFlagGroups(flagGroups []*crosstoolpb.CToolchain_FlagGroup, depth int) string {
+	var res []string
+	for _, flagGroup := range flagGroups {
+		res = append(res, parseFlagGroup(flagGroup, depth+1))
+	}
+	return makeStringArr(res, depth /* isPlainString= */, false)
+}
+
+func parseFlagGroup(flagGroup *crosstoolpb.CToolchain_FlagGroup, depth int) string {
+	var res []string
+	if len(flagGroup.GetFlag()) != 0 {
+		res = append(res, "flags = "+makeStringArr(flagGroup.GetFlag(), depth+1, true))
+	}
+	if flagGroup.GetIterateOver() != "" {
+		res = append(res, fmt.Sprintf("iterate_over = \"%s\"", flagGroup.GetIterateOver()))
+	}
+	if len(flagGroup.GetFlagGroup()) != 0 {
+		res = append(res, "flag_groups = "+parseFlagGroups(flagGroup.GetFlagGroup(), depth+1))
+	}
+	if len(flagGroup.GetExpandIfAllAvailable()) != 0 {
+		res = append(res,
+			fmt.Sprintf(
+				"expand_if_available = \"%s\"",
+				flagGroup.GetExpandIfAllAvailable()[0]))
+	}
+	if len(flagGroup.GetExpandIfNoneAvailable()) != 0 {
+		res = append(res,
+			fmt.Sprintf(
+				"expand_if_not_available = \"%s\"",
+				flagGroup.GetExpandIfNoneAvailable()[0]))
+	}
+	if flagGroup.GetExpandIfTrue() != "" {
+		res = append(res, fmt.Sprintf("expand_if_true = \"%s\"",
+			flagGroup.GetExpandIfTrue()))
+	}
+	if flagGroup.GetExpandIfFalse() != "" {
+		res = append(res, fmt.Sprintf("expand_if_false = \"%s\"",
+			flagGroup.GetExpandIfFalse()))
+	}
+	if flagGroup.GetExpandIfEqual() != nil {
+		res = append(res,
+			"expand_if_equal = "+parseVariableWithValue(
+				flagGroup.GetExpandIfEqual(), depth+1))
+	}
+	return createObject("flag_group", res, depth)
+}
+
+func parseVariableWithValue(variable *crosstoolpb.CToolchain_VariableWithValue, depth int) string {
+	variableName := fmt.Sprintf("name = \"%s\"", variable.GetVariable())
+	value := fmt.Sprintf("value = \"%s\"", variable.GetValue())
+	return createObject("variable_with_value", []string{variableName, value}, depth)
+}
+
+func getToolPaths(crosstool *crosstoolpb.CrosstoolRelease,
+	cToolchainIdentifiers map[string]CToolchainIdentifier, depth int) string {
+	var res []string
+	toolPathsToIds := make(map[string][]string)
+	for _, toolchain := range crosstool.GetToolchain() {
+		toolPaths := parseToolPaths(toolchain.GetToolPath(), depth)
+		toolPathsToIds[toolPaths] = append(
+			toolPathsToIds[toolPaths],
+			toolchain.GetToolchainIdentifier())
+	}
+	res = append(res,
+		getAssignmentStatement(
+			"tool_paths",
+			toolPathsToIds,
+			crosstool,
+			cToolchainIdentifiers,
+			depth,
+			/* isPlainString= */ false))
+	return strings.Join(res, "\n")
+}
+
+func parseToolPaths(toolPaths []*crosstoolpb.ToolPath, depth int) string {
+	var res []string
+	for _, toolPath := range toolPaths {
+		res = append(res, parseToolPath(toolPath, depth+1))
+	}
+	return makeStringArr(res, depth /* isPlainString= */, false)
+}
+
+func parseToolPath(toolPath *crosstoolpb.ToolPath, depth int) string {
+	name := fmt.Sprintf("name = \"%s\"", toolPath.GetName())
+	path := toolPath.GetPath()
+	if path == "" {
+		path = "NOT_USED"
+	}
+	path = fmt.Sprintf("path = \"%s\"", path)
+	return createObject("tool_path", []string{name, path}, depth)
+}
+
+func getMakeVariables(crosstool *crosstoolpb.CrosstoolRelease,
+	cToolchainIdentifiers map[string]CToolchainIdentifier, depth int) string {
+	var res []string
+	makeVariablesToIds := make(map[string][]string)
+	for _, toolchain := range crosstool.GetToolchain() {
+		makeVariables := parseMakeVariables(toolchain.GetMakeVariable(), depth)
+		makeVariablesToIds[makeVariables] = append(
+			makeVariablesToIds[makeVariables],
+			toolchain.GetToolchainIdentifier())
+	}
+	res = append(res,
+		getAssignmentStatement(
+			"make_variables",
+			makeVariablesToIds,
+			crosstool,
+			cToolchainIdentifiers,
+			depth,
+			/* isPlainString= */ false))
+	return strings.Join(res, "\n")
+}
+
+func parseMakeVariables(makeVariables []*crosstoolpb.MakeVariable, depth int) string {
+	var res []string
+	for _, makeVariable := range makeVariables {
+		res = append(res, parseMakeVariable(makeVariable, depth+1))
+	}
+	return makeStringArr(res, depth /* isPlainString= */, false)
+}
+
+func parseMakeVariable(makeVariable *crosstoolpb.MakeVariable, depth int) string {
+	name := fmt.Sprintf("name = \"%s\"", makeVariable.GetName())
+	value := fmt.Sprintf("value = \"%s\"", makeVariable.GetValue())
+	return createObject("make_variable", []string{name, value}, depth)
+}
+
+func parseTools(tools []*crosstoolpb.CToolchain_Tool, depth int) string {
+	var res []string
+	for _, tool := range tools {
+		res = append(res, parseTool(tool, depth+1))
+	}
+	return makeStringArr(res, depth /* isPlainString= */, false)
+}
+
+func parseTool(tool *crosstoolpb.CToolchain_Tool, depth int) string {
+	toolPath := "path = \"NOT_USED\""
+	if tool.GetToolPath() != "" {
+		toolPath = fmt.Sprintf("path = \"%s\"", tool.GetToolPath())
+	}
+	fields := []string{toolPath}
+	if len(tool.GetWithFeature()) != 0 {
+		withFeatures := "with_features = " + parseWithFeatureSets(tool.GetWithFeature(), depth+1)
+		fields = append(fields, withFeatures)
+	}
+	if len(tool.GetExecutionRequirement()) != 0 {
+		executionRequirements := "execution_requirements = " +
+			makeStringArr(tool.GetExecutionRequirement(), depth+1 /* isPlainString= */, true)
+		fields = append(fields, executionRequirements)
+	}
+	return createObject("tool", fields, depth)
+}
+
+func parseEnvEntries(envEntries []*crosstoolpb.CToolchain_EnvEntry, depth int) string {
+	var res []string
+	for _, envEntry := range envEntries {
+		res = append(res, parseEnvEntry(envEntry, depth+1))
+	}
+	return makeStringArr(res, depth /* isPlainString= */, false)
+}
+
+func parseEnvEntry(envEntry *crosstoolpb.CToolchain_EnvEntry, depth int) string {
+	key := fmt.Sprintf("key = \"%s\"", envEntry.GetKey())
+	value := fmt.Sprintf("value = \"%s\"", envEntry.GetValue())
+	return createObject("env_entry", []string{key, value}, depth)
+}
+
+func parseWithFeatureSets(withFeatureSets []*crosstoolpb.CToolchain_WithFeatureSet,
+	depth int) string {
+	var res []string
+	for _, withFeature := range withFeatureSets {
+		res = append(res, parseWithFeatureSet(withFeature, depth+1))
+	}
+	return makeStringArr(res, depth /* isPlainString= */, false)
+}
+
+func parseWithFeatureSet(withFeature *crosstoolpb.CToolchain_WithFeatureSet,
+	depth int) string {
+	var fields []string
+	if len(withFeature.GetFeature()) != 0 {
+		features := "features = " +
+			makeStringArr(withFeature.GetFeature(), depth+1 /* isPlainString= */, true)
+		fields = append(fields, features)
+	}
+	if len(withFeature.GetNotFeature()) != 0 {
+		notFeatures := "not_features = " +
+			makeStringArr(withFeature.GetNotFeature(), depth+1 /* isPlainString= */, true)
+		fields = append(fields, notFeatures)
+	}
+	return createObject("with_feature_set", fields, depth)
+}
+
+func parseEnvSets(envSets []*crosstoolpb.CToolchain_EnvSet, depth int) string {
+	var res []string
+	for _, envSet := range envSets {
+		envSetString := parseEnvSet(envSet, depth+1)
+		res = append(res, envSetString)
+	}
+	return makeStringArr(res, depth /* isPlainString= */, false)
+}
+
+func parseEnvSet(envSet *crosstoolpb.CToolchain_EnvSet, depth int) string {
+	actionsStatement := processActions(envSet.GetAction(), depth)
+	actions := "actions = " + strings.Join(actionsStatement, " +\n"+getTabs(depth+2))
+	fields := []string{actions}
+	if len(envSet.GetEnvEntry()) != 0 {
+		envEntries := "env_entries = " + parseEnvEntries(envSet.GetEnvEntry(), depth+1)
+		fields = append(fields, envEntries)
+	}
+	if len(envSet.GetWithFeature()) != 0 {
+		withFeatures := "with_features = " + parseWithFeatureSets(envSet.GetWithFeature(), depth+1)
+		fields = append(fields, withFeatures)
+	}
+	return createObject("env_set", fields, depth)
+}
+
+func parseFeatureSets(featureSets []*crosstoolpb.CToolchain_FeatureSet, depth int) string {
+	var res []string
+	for _, featureSet := range featureSets {
+		res = append(res, parseFeatureSet(featureSet, depth+1))
+	}
+	return makeStringArr(res, depth /* isPlainString= */, false)
+}
+
+func parseFeatureSet(featureSet *crosstoolpb.CToolchain_FeatureSet, depth int) string {
+	features := "features = " +
+		makeStringArr(featureSet.GetFeature(), depth+1 /* isPlainString= */, true)
+	return createObject("feature_set", []string{features}, depth)
+}
+
+// Takes in a list of string elements and returns a string that represents
+// an array :
+//     [
+//         "element1",
+//         "element2",
+//     ]
+// The isPlainString argument tells us whether the input elements should be
+// treated as string (eg, flags), or not (eg, variable names)
+func makeStringArr(arr []string, depth int, isPlainString bool) string {
+	if len(arr) == 0 {
+		return "[]"
+	}
+	var escapedArr []string
+	for _, el := range arr {
+		if isPlainString {
+			escapedArr = append(escapedArr, strings.Replace(el, "\"", "\\\"", -1))
+		} else {
+			escapedArr = append(escapedArr, el)
+		}
+	}
+	addQuote := ""
+	if isPlainString {
+		addQuote = "\""
+	}
+	singleLine := "[" + addQuote + strings.Join(escapedArr, addQuote+", "+addQuote) + addQuote + "]"
+	if len(singleLine) < 60 {
+		return singleLine
+	}
+	return "[\n" +
+		getTabs(depth+1) +
+		addQuote +
+		strings.Join(escapedArr, addQuote+",\n"+getTabs(depth+1)+addQuote) +
+		addQuote +
+		",\n" +
+		getTabs(depth) +
+		"]"
+}
+
+// Returns a string that represents a value assignment
+// (eg if ctx.attr.cpu == "linux":
+//         compiler = "llvm"
+//     elif ctx.attr.cpu == "windows":
+//         compiler = "mingw"
+//     else:
+//         fail("Unreachable")
+func getAssignmentStatement(field string, valToIds map[string][]string,
+	crosstool *crosstoolpb.CrosstoolRelease,
+	toCToolchainIdentifier map[string]CToolchainIdentifier,
+	depth int, isPlainString bool) string {
+	var b bytes.Buffer
+	if len(valToIds) <= 1 {
+		// if there is only one possible value for this field, we don't need if statements
+		for val := range valToIds {
+			if val != "None" && isPlainString {
+				val = "\"" + val + "\""
+			}
+			b.WriteString(fmt.Sprintf("%s%s = %s\n", getTabs(depth), field, val))
+			break
+		}
+	} else {
+		first := true
+		var keys []string
+		for k := range valToIds {
+			keys = append(keys, k)
+		}
+		sort.Strings(keys)
+		for _, value := range keys {
+			ids := valToIds[value]
+			branch := "elif"
+			if first {
+				branch = "if"
+			}
+			b.WriteString(
+				getIfStatement(branch, ids, field, value,
+					toCToolchainIdentifier, depth, isPlainString))
+			first = false
+		}
+		b.WriteString(
+			fmt.Sprintf(
+				"%selse:\n%sfail(\"Unreachable\")\n",
+				getTabs(depth), getTabs(depth+1)))
+	}
+	b.WriteString("\n")
+	return b.String()
+}
+
+func getCPUToCompilers(identifiers []CToolchainIdentifier) map[string][]string {
+	res := make(map[string][]string)
+	for _, identifier := range identifiers {
+		if identifier.compiler != "" {
+			res[identifier.cpu] = append(res[identifier.cpu], identifier.compiler)
+		}
+	}
+	return res
+}
+
+func getIfStatement(ifOrElseIf string, identifiers []string, field, val string,
+	toCToolchainIdentifier map[string]CToolchainIdentifier, depth int,
+	isPlainString bool) string {
+	usedStmts := make(map[string]bool)
+	if val != "None" && isPlainString {
+		val = "\"" + val + "\""
+	}
+	var cToolchainIdentifiers []CToolchainIdentifier
+	for _, value := range toCToolchainIdentifier {
+		cToolchainIdentifiers = append(cToolchainIdentifiers, value)
+	}
+	cpuToCompilers := getCPUToCompilers(cToolchainIdentifiers)
+	countCpus := make(map[string]int)
+	var conditions []string
+	for _, id := range identifiers {
+		identifier := toCToolchainIdentifier[id]
+		stmt := getConditionStatementForCToolchainIdentifier(identifier)
+		if _, ok := usedStmts[stmt]; !ok {
+			conditions = append(conditions, stmt)
+			usedStmts[stmt] = true
+			if identifier.compiler != "" {
+				countCpus[identifier.cpu]++
+			}
+		}
+	}
+
+	var compressedConditions []string
+	usedStmtsOptimized := make(map[string]bool)
+	for _, id := range identifiers {
+		identifier := toCToolchainIdentifier[id]
+		var stmt string
+		if _, ok := countCpus[identifier.cpu]; ok {
+			if countCpus[identifier.cpu] == len(cpuToCompilers[identifier.cpu]) {
+				stmt = getConditionStatementForCToolchainIdentifier(
+					CToolchainIdentifier{cpu: identifier.cpu, compiler: ""})
+			} else {
+				stmt = getConditionStatementForCToolchainIdentifier(identifier)
+			}
+		} else {
+			stmt = getConditionStatementForCToolchainIdentifier(identifier)
+		}
+		if _, ok := usedStmtsOptimized[stmt]; !ok {
+			compressedConditions = append(compressedConditions, stmt)
+			usedStmtsOptimized[stmt] = true
+		}
+	}
+
+	sort.Strings(compressedConditions)
+	val = strings.Join(strings.Split(val, "\n"+getTabs(depth)), "\n"+getTabs(depth+1))
+	return fmt.Sprintf(`%s%s %s:
+%s%s = %s
+`, getTabs(depth),
+		ifOrElseIf,
+		"("+strings.Join(compressedConditions, "\n"+getTabs(depth+1)+"or ")+")",
+		getTabs(depth+1),
+		field,
+		val)
+}
+
+func getToolchainIdentifiers(crosstool *crosstoolpb.CrosstoolRelease) []string {
+	var res []string
+	for _, toolchain := range crosstool.GetToolchain() {
+		res = append(res, toolchain.GetToolchainIdentifier())
+	}
+	return res
+}
+
+func getHostSystemNames(crosstool *crosstoolpb.CrosstoolRelease) []string {
+	var res []string
+	for _, toolchain := range crosstool.GetToolchain() {
+		res = append(res, toolchain.GetHostSystemName())
+	}
+	return res
+}
+
+func getTargetSystemNames(crosstool *crosstoolpb.CrosstoolRelease) []string {
+	var res []string
+	for _, toolchain := range crosstool.GetToolchain() {
+		res = append(res, toolchain.GetTargetSystemName())
+	}
+	return res
+}
+
+func getTargetCpus(crosstool *crosstoolpb.CrosstoolRelease) []string {
+	var res []string
+	for _, toolchain := range crosstool.GetToolchain() {
+		res = append(res, toolchain.GetTargetCpu())
+	}
+	return res
+}
+
+func getTargetLibcs(crosstool *crosstoolpb.CrosstoolRelease) []string {
+	var res []string
+	for _, toolchain := range crosstool.GetToolchain() {
+		res = append(res, toolchain.GetTargetLibc())
+	}
+	return res
+}
+
+func getCompilers(crosstool *crosstoolpb.CrosstoolRelease) []string {
+	var res []string
+	for _, toolchain := range crosstool.GetToolchain() {
+		res = append(res, toolchain.GetCompiler())
+	}
+	return res
+}
+
+func getAbiVersions(crosstool *crosstoolpb.CrosstoolRelease) []string {
+	var res []string
+	for _, toolchain := range crosstool.GetToolchain() {
+		res = append(res, toolchain.GetAbiVersion())
+	}
+	return res
+}
+
+func getAbiLibcVersions(crosstool *crosstoolpb.CrosstoolRelease) []string {
+	var res []string
+	for _, toolchain := range crosstool.GetToolchain() {
+		res = append(res, toolchain.GetAbiLibcVersion())
+	}
+	return res
+}
+
+func getCcTargetOss(crosstool *crosstoolpb.CrosstoolRelease) []string {
+	var res []string
+	for _, toolchain := range crosstool.GetToolchain() {
+		targetOS := "None"
+		if toolchain.GetCcTargetOs() != "" {
+			targetOS = toolchain.GetCcTargetOs()
+		}
+		res = append(res, targetOS)
+	}
+	return res
+}
+
+func getBuiltinSysroots(crosstool *crosstoolpb.CrosstoolRelease) []string {
+	var res []string
+	for _, toolchain := range crosstool.GetToolchain() {
+		sysroot := "None"
+		if toolchain.GetBuiltinSysroot() != "" {
+			sysroot = toolchain.GetBuiltinSysroot()
+		}
+		res = append(res, sysroot)
+	}
+	return res
+}
+
+func getMappedStringValuesToIdentifiers(identifiers, fields []string) map[string][]string {
+	res := make(map[string][]string)
+	for i := range identifiers {
+		res[fields[i]] = append(res[fields[i]], identifiers[i])
+	}
+	return res
+}
+
+func getReturnStatement() string {
+	return `
+    out = ctx.actions.declare_file(ctx.label.name)
+    ctx.actions.write(out, "Fake executable")
+    return [
+        cc_common.create_cc_toolchain_config_info(
+            ctx = ctx,
+            features = features,
+            action_configs = action_configs,
+            artifact_name_patterns = artifact_name_patterns,
+            cxx_builtin_include_directories = cxx_builtin_include_directories,
+            toolchain_identifier = toolchain_identifier,
+            host_system_name = host_system_name,
+            target_system_name = target_system_name,
+            target_cpu = target_cpu,
+            target_libc = target_libc,
+            compiler = compiler,
+            abi_version = abi_version,
+            abi_libc_version = abi_libc_version,
+            tool_paths = tool_paths,
+            make_variables = make_variables,
+            builtin_sysroot = builtin_sysroot,
+            cc_target_os = cc_target_os
+        ),
+        DefaultInfo(
+            executable = out,
+        ),
+    ]
+`
+}
+
+// Transform writes a cc_toolchain_config rule functionally equivalent to the
+// CROSSTOOL file.
+func Transform(crosstool *crosstoolpb.CrosstoolRelease) (string, error) {
+	var b bytes.Buffer
+
+	cToolchainIdentifiers := toolchainToCToolchainIdentifier(crosstool)
+
+	toolchainToFeatures, featureNameToFeature := getFeatures(crosstool)
+
+	toolchainToActions, actionNameToAction := getActions(crosstool)
+
+	header := getCcToolchainConfigHeader()
+	if _, err := b.WriteString(header); err != nil {
+		return "", err
+	}
+
+	loadActionsStmt := getLoadActionsStmt()
+	if _, err := b.WriteString(loadActionsStmt); err != nil {
+		return "", err
+	}
+
+	implHeader := getImplHeader()
+	if _, err := b.WriteString(implHeader); err != nil {
+		return "", err
+	}
+
+	stringFields := []string{
+		"toolchain_identifier",
+		"host_system_name",
+		"target_system_name",
+		"target_cpu",
+		"target_libc",
+		"compiler",
+		"abi_version",
+		"abi_libc_version",
+		"cc_target_os",
+		"builtin_sysroot",
+	}
+
+	for _, stringField := range stringFields {
+		stmt := getStringStatement(crosstool, cToolchainIdentifiers, stringField, 1)
+		if _, err := b.WriteString(stmt); err != nil {
+			return "", err
+		}
+	}
+
+	listsOfActions := []string{
+		"all_compile_actions",
+		"all_cpp_compile_actions",
+		"preprocessor_compile_actions",
+		"codegen_compile_actions",
+		"all_link_actions",
+	}
+
+	for _, listOfActions := range listsOfActions {
+		actions := getListOfActions(listOfActions, 1)
+		if _, err := b.WriteString(actions); err != nil {
+			return "", err
+		}
+	}
+
+	actionConfigDeclaration := getActionConfigsDeclaration(
+		crosstool, cToolchainIdentifiers, actionNameToAction, 1)
+	if _, err := b.WriteString(actionConfigDeclaration); err != nil {
+		return "", err
+	}
+
+	actionConfigStatement := getActionConfigsStmt(
+		cToolchainIdentifiers, toolchainToActions, 1)
+	if _, err := b.WriteString(actionConfigStatement); err != nil {
+		return "", err
+	}
+
+	featureDeclaration := getFeaturesDeclaration(
+		crosstool, cToolchainIdentifiers, featureNameToFeature, 1)
+	if _, err := b.WriteString(featureDeclaration); err != nil {
+		return "", err
+	}
+
+	featuresStatement := getFeaturesStmt(
+		cToolchainIdentifiers, toolchainToFeatures, 1)
+	if _, err := b.WriteString(featuresStatement); err != nil {
+		return "", err
+	}
+
+	includeDirectories := getStringArr(
+		crosstool, cToolchainIdentifiers, "cxx_builtin_include_directories", 1)
+	if _, err := b.WriteString(includeDirectories); err != nil {
+		return "", err
+	}
+
+	artifactNamePatterns := getArtifactNamePatterns(
+		crosstool, cToolchainIdentifiers, 1)
+	if _, err := b.WriteString(artifactNamePatterns); err != nil {
+		return "", err
+	}
+
+	makeVariables := getMakeVariables(crosstool, cToolchainIdentifiers, 1)
+	if _, err := b.WriteString(makeVariables); err != nil {
+		return "", err
+	}
+
+	toolPaths := getToolPaths(crosstool, cToolchainIdentifiers, 1)
+	if _, err := b.WriteString(toolPaths); err != nil {
+		return "", err
+	}
+
+	if _, err := b.WriteString(getReturnStatement()); err != nil {
+		return "", err
+	}
+
+	rule := getRule(cToolchainIdentifiers)
+	if _, err := b.WriteString(rule); err != nil {
+		return "", err
+	}
+
+	return b.String(), nil
+}
diff --git a/tools/migration/crosstool_to_starlark_lib_test.go b/tools/migration/crosstool_to_starlark_lib_test.go
new file mode 100644
index 0000000..ddb082b
--- /dev/null
+++ b/tools/migration/crosstool_to_starlark_lib_test.go
@@ -0,0 +1,1097 @@
+package crosstooltostarlarklib
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+
+	"log"
+	"github.com/golang/protobuf/proto"
+	crosstoolpb "third_party/com/github/bazelbuild/bazel/src/main/protobuf/crosstool_config_go_proto"
+)
+
+func makeCToolchainString(lines []string) string {
+	return fmt.Sprintf(`toolchain {
+  %s
+}`, strings.Join(lines, "\n  "))
+}
+
+func makeCrosstool(CToolchains []string) *crosstoolpb.CrosstoolRelease {
+	crosstool := &crosstoolpb.CrosstoolRelease{}
+	requiredFields := []string{
+		"major_version: '0'",
+		"minor_version: '0'",
+		"default_target_cpu: 'cpu'",
+	}
+	CToolchains = append(CToolchains, requiredFields...)
+	if err := proto.UnmarshalText(strings.Join(CToolchains, "\n"), crosstool); err != nil {
+		log.Fatalf("Failed to parse CROSSTOOL:", err)
+	}
+	return crosstool
+}
+
+func getSimpleCToolchain(id string) string {
+	lines := []string{
+		"toolchain_identifier: 'id-" + id + "'",
+		"host_system_name: 'host-" + id + "'",
+		"target_system_name: 'target-" + id + "'",
+		"target_cpu: 'cpu-" + id + "'",
+		"compiler: 'compiler-" + id + "'",
+		"target_libc: 'libc-" + id + "'",
+		"abi_version: 'version-" + id + "'",
+		"abi_libc_version: 'libc_version-" + id + "'",
+	}
+	return makeCToolchainString(lines)
+}
+
+func getCToolchain(id, cpu, compiler string, extraLines []string) string {
+	lines := []string{
+		"toolchain_identifier: '" + id + "'",
+		"host_system_name: 'host'",
+		"target_system_name: 'target'",
+		"target_cpu: '" + cpu + "'",
+		"compiler: '" + compiler + "'",
+		"target_libc: 'libc'",
+		"abi_version: 'version'",
+		"abi_libc_version: 'libc_version'",
+	}
+	lines = append(lines, extraLines...)
+	return makeCToolchainString(lines)
+}
+
+func TestStringFieldsConditionStatement(t *testing.T) {
+	toolchain1 := getSimpleCToolchain("1")
+	toolchain2 := getSimpleCToolchain("2")
+	toolchains := []string{toolchain1, toolchain2}
+	crosstool := makeCrosstool(toolchains)
+
+	testCases := []struct {
+		field        string
+		expectedText string
+	}{
+		{field: "toolchain_identifier",
+			expectedText: `
+    if (ctx.attr.cpu == "cpu-1"):
+        toolchain_identifier = "id-1"
+    elif (ctx.attr.cpu == "cpu-2"):
+        toolchain_identifier = "id-2"
+    else:
+        fail("Unreachable")`},
+		{field: "host_system_name",
+			expectedText: `
+    if (ctx.attr.cpu == "cpu-1"):
+        host_system_name = "host-1"
+    elif (ctx.attr.cpu == "cpu-2"):
+        host_system_name = "host-2"
+    else:
+        fail("Unreachable")`},
+		{field: "target_system_name",
+			expectedText: `
+    if (ctx.attr.cpu == "cpu-1"):
+        target_system_name = "target-1"
+    elif (ctx.attr.cpu == "cpu-2"):
+        target_system_name = "target-2"
+    else:
+        fail("Unreachable")`},
+		{field: "target_cpu",
+			expectedText: `
+    if (ctx.attr.cpu == "cpu-1"):
+        target_cpu = "cpu-1"
+    elif (ctx.attr.cpu == "cpu-2"):
+        target_cpu = "cpu-2"
+    else:
+        fail("Unreachable")`},
+		{field: "target_libc",
+			expectedText: `
+    if (ctx.attr.cpu == "cpu-1"):
+        target_libc = "libc-1"
+    elif (ctx.attr.cpu == "cpu-2"):
+        target_libc = "libc-2"
+    else:
+        fail("Unreachable")`},
+		{field: "compiler",
+			expectedText: `
+    if (ctx.attr.cpu == "cpu-1"):
+        compiler = "compiler-1"
+    elif (ctx.attr.cpu == "cpu-2"):
+        compiler = "compiler-2"
+    else:
+        fail("Unreachable")`},
+		{field: "abi_version",
+			expectedText: `
+    if (ctx.attr.cpu == "cpu-1"):
+        abi_version = "version-1"
+    elif (ctx.attr.cpu == "cpu-2"):
+        abi_version = "version-2"
+    else:
+        fail("Unreachable")`},
+		{field: "abi_libc_version",
+			expectedText: `
+    if (ctx.attr.cpu == "cpu-1"):
+        abi_libc_version = "libc_version-1"
+    elif (ctx.attr.cpu == "cpu-2"):
+        abi_libc_version = "libc_version-2"
+    else:
+        fail("Unreachable")`}}
+
+	got, err := Transform(crosstool)
+	if err != nil {
+		t.Fatalf("CROSSTOOL conversion failed: %v", err)
+	}
+
+	failed := false
+	for _, tc := range testCases {
+		if !strings.Contains(got, tc.expectedText) {
+			t.Errorf("Failed to correctly convert '%s' field, expected to contain:\n%v\n",
+				tc.field, tc.expectedText)
+			failed = true
+		}
+	}
+	if failed {
+		t.Fatalf("Tested CROSSTOOL:\n%v\n\nGenerated rule:\n%v\n",
+			strings.Join(toolchains, "\n"), got)
+	}
+}
+
+func TestConditionsSameCpu(t *testing.T) {
+	toolchainAA := getCToolchain("1", "cpuA", "compilerA", []string{})
+	toolchainAB := getCToolchain("2", "cpuA", "compilerB", []string{})
+	toolchains := []string{toolchainAA, toolchainAB}
+	crosstool := makeCrosstool(toolchains)
+
+	testCases := []struct {
+		field        string
+		expectedText string
+	}{
+		{field: "toolchain_identifier",
+			expectedText: `
+    if (ctx.attr.cpu == "cpuA" and ctx.attr.compiler == "compilerA"):
+        toolchain_identifier = "1"
+    elif (ctx.attr.cpu == "cpuA" and ctx.attr.compiler == "compilerB"):
+        toolchain_identifier = "2"
+    else:
+        fail("Unreachable")`},
+		{field: "host_system_name",
+			expectedText: `
+    host_system_name = "host"`},
+		{field: "target_system_name",
+			expectedText: `
+    target_system_name = "target"`},
+		{field: "target_cpu",
+			expectedText: `
+    target_cpu = "cpuA"`},
+		{field: "target_libc",
+			expectedText: `
+    target_libc = "libc"`},
+		{field: "compiler",
+			expectedText: `
+    if (ctx.attr.cpu == "cpuA" and ctx.attr.compiler == "compilerA"):
+        compiler = "compilerA"
+    elif (ctx.attr.cpu == "cpuA" and ctx.attr.compiler == "compilerB"):
+        compiler = "compilerB"
+    else:
+        fail("Unreachable")`},
+		{field: "abi_version",
+			expectedText: `
+    abi_version = "version"`},
+		{field: "abi_libc_version",
+			expectedText: `
+    abi_libc_version = "libc_version"`}}
+
+	got, err := Transform(crosstool)
+	if err != nil {
+		t.Fatalf("CROSSTOOL conversion failed: %v", err)
+	}
+
+	failed := false
+	for _, tc := range testCases {
+		if !strings.Contains(got, tc.expectedText) {
+			t.Errorf("Failed to correctly convert '%s' field, expected to contain:\n%v\n",
+				tc.field, tc.expectedText)
+			failed = true
+		}
+	}
+	if failed {
+		t.Fatalf("Tested CROSSTOOL:\n%v\n\nGenerated rule:\n%v\n",
+			strings.Join(toolchains, "\n"), got)
+	}
+}
+
+func TestConditionsSameCompiler(t *testing.T) {
+	toolchainAA := getCToolchain("1", "cpuA", "compilerA", []string{})
+	toolchainBA := getCToolchain("2", "cpuB", "compilerA", []string{})
+	toolchains := []string{toolchainAA, toolchainBA}
+	crosstool := makeCrosstool(toolchains)
+
+	testCases := []struct {
+		field        string
+		expectedText string
+	}{
+		{field: "toolchain_identifier",
+			expectedText: `
+    if (ctx.attr.cpu == "cpuA"):
+        toolchain_identifier = "1"
+    elif (ctx.attr.cpu == "cpuB"):
+        toolchain_identifier = "2"
+    else:
+        fail("Unreachable")`},
+		{field: "target_cpu",
+			expectedText: `
+    if (ctx.attr.cpu == "cpuA"):
+        target_cpu = "cpuA"
+    elif (ctx.attr.cpu == "cpuB"):
+        target_cpu = "cpuB"
+    else:
+        fail("Unreachable")`},
+		{field: "compiler",
+			expectedText: `
+    compiler = "compilerA"`}}
+
+	got, err := Transform(crosstool)
+	if err != nil {
+		t.Fatalf("CROSSTOOL conversion failed: %v", err)
+	}
+
+	failed := false
+	for _, tc := range testCases {
+		if !strings.Contains(got, tc.expectedText) {
+			t.Errorf("Failed to correctly convert '%s' field, expected to contain:\n%v\n",
+				tc.field, tc.expectedText)
+			failed = true
+		}
+	}
+	if failed {
+		t.Fatalf("Tested CROSSTOOL:\n%v\n\nGenerated rule:\n%v\n",
+			strings.Join(toolchains, "\n"), got)
+	}
+}
+
+func TestNonMandatoryStrings(t *testing.T) {
+	toolchainAA := getCToolchain("1", "cpuA", "compilerA", []string{"cc_target_os: 'osA'"})
+	toolchainBB := getCToolchain("2", "cpuB", "compilerB", []string{})
+	toolchains := []string{toolchainAA, toolchainBB}
+	crosstool := makeCrosstool(toolchains)
+
+	testCases := []struct {
+		field        string
+		expectedText string
+	}{
+		{field: "cc_target_os",
+			expectedText: `
+    if (ctx.attr.cpu == "cpuB"):
+        cc_target_os = None
+    elif (ctx.attr.cpu == "cpuA"):
+        cc_target_os = "osA"
+    else:
+        fail("Unreachable")`},
+		{field: "builtin_sysroot",
+			expectedText: `
+    builtin_sysroot = None`}}
+
+	got, err := Transform(crosstool)
+	if err != nil {
+		t.Fatalf("CROSSTOOL conversion failed: %v", err)
+	}
+
+	failed := false
+	for _, tc := range testCases {
+		if !strings.Contains(got, tc.expectedText) {
+			t.Errorf("Failed to correctly convert '%s' field, expected to contain:\n%v\n",
+				tc.field, tc.expectedText)
+			failed = true
+		}
+	}
+	if failed {
+		t.Fatalf("Tested CROSSTOOL:\n%v\n\nGenerated rule:\n%v\n",
+			strings.Join(toolchains, "\n"), got)
+	}
+}
+
+func TestBuiltinIncludeDirectories(t *testing.T) {
+	toolchainAA := getCToolchain("1", "cpuA", "compilerA", []string{})
+	toolchainBA := getCToolchain("2", "cpuB", "compilerA", []string{})
+	toolchainCA := getCToolchain("3", "cpuC", "compilerA",
+		[]string{"cxx_builtin_include_directory: 'dirC'"})
+	toolchainCB := getCToolchain("4", "cpuC", "compilerB",
+		[]string{"cxx_builtin_include_directory: 'dirC'",
+			"cxx_builtin_include_directory: 'dirB'"})
+	toolchainDA := getCToolchain("5", "cpuD", "compilerA",
+		[]string{"cxx_builtin_include_directory: 'dirC'"})
+
+	toolchainsEmpty := []string{toolchainAA, toolchainBA}
+
+	toolchainsOneNonempty := []string{toolchainAA, toolchainBA, toolchainCA}
+
+	toolchainsSameNonempty := []string{toolchainCA, toolchainDA}
+
+	allToolchains := []string{toolchainAA, toolchainBA, toolchainCA, toolchainCB, toolchainDA}
+
+	testCases := []struct {
+		field        string
+		toolchains   []string
+		expectedText string
+	}{
+		{field: "cxx_builtin_include_directories",
+			toolchains: toolchainsEmpty,
+			expectedText: `
+    cxx_builtin_include_directories = []`},
+		{field: "cxx_builtin_include_directories",
+			toolchains: toolchainsOneNonempty,
+			expectedText: `
+    if (ctx.attr.cpu == "cpuA"
+        or ctx.attr.cpu == "cpuB"):
+        cxx_builtin_include_directories = []
+    elif (ctx.attr.cpu == "cpuC"):
+        cxx_builtin_include_directories = ["dirC"]
+    else:
+        fail("Unreachable")`},
+		{field: "cxx_builtin_include_directories",
+			toolchains: toolchainsSameNonempty,
+			expectedText: `
+    cxx_builtin_include_directories = ["dirC"]`},
+		{field: "cxx_builtin_include_directories",
+			toolchains: allToolchains,
+			expectedText: `
+    if (ctx.attr.cpu == "cpuA"
+        or ctx.attr.cpu == "cpuB"):
+        cxx_builtin_include_directories = []
+    elif (ctx.attr.cpu == "cpuC" and ctx.attr.compiler == "compilerA"
+        or ctx.attr.cpu == "cpuD"):
+        cxx_builtin_include_directories = ["dirC"]
+    elif (ctx.attr.cpu == "cpuC" and ctx.attr.compiler == "compilerB"):
+        cxx_builtin_include_directories = ["dirC", "dirB"]`}}
+
+	for _, tc := range testCases {
+		crosstool := makeCrosstool(tc.toolchains)
+		got, err := Transform(crosstool)
+		if err != nil {
+			t.Fatalf("CROSSTOOL conversion failed: %v", err)
+		}
+		if !strings.Contains(got, tc.expectedText) {
+			t.Errorf("Failed to correctly convert '%s' field, expected to contain:\n%v\n",
+				tc.field, tc.expectedText)
+			t.Fatalf("Tested CROSSTOOL:\n%v\n\nGenerated rule:\n%v\n",
+				strings.Join(tc.toolchains, "\n"), got)
+		}
+	}
+}
+
+func TestMakeVariables(t *testing.T) {
+	toolchainEmpty1 := getCToolchain("1", "cpuA", "compilerA", []string{})
+	toolchainEmpty2 := getCToolchain("2", "cpuB", "compilerA", []string{})
+	toolchainA1 := getCToolchain("3", "cpuC", "compilerA",
+		[]string{"make_variable {name: 'A', value: 'a/b/c'}"})
+	toolchainA2 := getCToolchain("4", "cpuC", "compilerB",
+		[]string{"make_variable {name: 'A', value: 'a/b/c'}"})
+	toolchainAB := getCToolchain("5", "cpuC", "compilerC",
+		[]string{"make_variable {name: 'A', value: 'a/b/c'}",
+			"make_variable {name: 'B', value: 'a/b/c'}"})
+	toolchainBA := getCToolchain("6", "cpuD", "compilerA",
+		[]string{"make_variable {name: 'B', value: 'a/b/c'}",
+			"make_variable {name: 'A', value: 'a b c'}"})
+
+	toolchainsEmpty := []string{toolchainEmpty1, toolchainEmpty2}
+
+	toolchainsOneNonempty := []string{toolchainEmpty1, toolchainA1}
+
+	toolchainsSameNonempty := []string{toolchainA1, toolchainA2}
+
+	toolchainsDifferentOrder := []string{toolchainAB, toolchainBA}
+
+	allToolchains := []string{
+		toolchainEmpty1,
+		toolchainEmpty2,
+		toolchainA1,
+		toolchainA2,
+		toolchainAB,
+		toolchainBA,
+	}
+
+	testCases := []struct {
+		field        string
+		toolchains   []string
+		expectedText string
+	}{
+		{field: "make_variables",
+			toolchains: toolchainsEmpty,
+			expectedText: `
+    make_variables = []`},
+		{field: "make_variables",
+			toolchains: toolchainsOneNonempty,
+			expectedText: `
+    if (ctx.attr.cpu == "cpuA"):
+        make_variables = []
+    elif (ctx.attr.cpu == "cpuC"):
+        make_variables = [make_variable(name = "A", value = "a/b/c")]
+    else:
+        fail("Unreachable")`},
+		{field: "make_variables",
+			toolchains: toolchainsSameNonempty,
+			expectedText: `
+    make_variables = [make_variable(name = "A", value = "a/b/c")]`},
+		{field: "make_variables",
+			toolchains: toolchainsDifferentOrder,
+			expectedText: `
+    if (ctx.attr.cpu == "cpuC"):
+        make_variables = [
+            make_variable(name = "A", value = "a/b/c"),
+            make_variable(name = "B", value = "a/b/c"),
+        ]
+    elif (ctx.attr.cpu == "cpuD"):
+        make_variables = [
+            make_variable(name = "B", value = "a/b/c"),
+            make_variable(name = "A", value = "a b c"),
+        ]
+    else:
+        fail("Unreachable")`},
+		{field: "make_variables",
+			toolchains: allToolchains,
+			expectedText: `
+    if (ctx.attr.cpu == "cpuC" and ctx.attr.compiler == "compilerC"):
+        make_variables = [
+            make_variable(name = "A", value = "a/b/c"),
+            make_variable(name = "B", value = "a/b/c"),
+        ]
+    elif (ctx.attr.cpu == "cpuD"):
+        make_variables = [
+            make_variable(name = "B", value = "a/b/c"),
+            make_variable(name = "A", value = "a b c"),
+        ]
+    elif (ctx.attr.cpu == "cpuA"
+        or ctx.attr.cpu == "cpuB"):
+        make_variables = []
+    elif (ctx.attr.cpu == "cpuC" and ctx.attr.compiler == "compilerA"
+        or ctx.attr.cpu == "cpuC" and ctx.attr.compiler == "compilerB"):
+        make_variables = [make_variable(name = "A", value = "a/b/c")]
+    else:
+        fail("Unreachable")`}}
+
+	for _, tc := range testCases {
+		crosstool := makeCrosstool(tc.toolchains)
+		got, err := Transform(crosstool)
+		if err != nil {
+			t.Fatalf("CROSSTOOL conversion failed: %v", err)
+		}
+		if !strings.Contains(got, tc.expectedText) {
+			t.Errorf("Failed to correctly convert '%s' field, expected to contain:\n%v\n",
+				tc.field, tc.expectedText)
+			t.Fatalf("Tested CROSSTOOL:\n%v\n\nGenerated rule:\n%v\n",
+				strings.Join(tc.toolchains, "\n"), got)
+		}
+	}
+}
+
+func TestToolPaths(t *testing.T) {
+	toolchainEmpty1 := getCToolchain("1", "cpuA", "compilerA", []string{})
+	toolchainEmpty2 := getCToolchain("2", "cpuB", "compilerA", []string{})
+	toolchainA1 := getCToolchain("3", "cpuC", "compilerA",
+		[]string{"tool_path {name: 'A', path: 'a/b/c'}"})
+	toolchainA2 := getCToolchain("4", "cpuC", "compilerB",
+		[]string{"tool_path {name: 'A', path: 'a/b/c'}"})
+	toolchainAB := getCToolchain("5", "cpuC", "compilerC",
+		[]string{"tool_path {name: 'A', path: 'a/b/c'}",
+			"tool_path {name: 'B', path: 'a/b/c'}"})
+	toolchainBA := getCToolchain("6", "cpuD", "compilerA",
+		[]string{"tool_path {name: 'B', path: 'a/b/c'}",
+			"tool_path {name: 'A', path: 'a/b/c'}"})
+
+	toolchainsEmpty := []string{toolchainEmpty1, toolchainEmpty2}
+
+	toolchainsOneNonempty := []string{toolchainEmpty1, toolchainA1}
+
+	toolchainsSameNonempty := []string{toolchainA1, toolchainA2}
+
+	toolchainsDifferentOrder := []string{toolchainAB, toolchainBA}
+
+	allToolchains := []string{
+		toolchainEmpty1,
+		toolchainEmpty2,
+		toolchainA1,
+		toolchainA2,
+		toolchainAB,
+		toolchainBA,
+	}
+
+	testCases := []struct {
+		field        string
+		toolchains   []string
+		expectedText string
+	}{
+		{field: "tool_paths",
+			toolchains: toolchainsEmpty,
+			expectedText: `
+    tool_paths = []`},
+		{field: "tool_paths",
+			toolchains: toolchainsOneNonempty,
+			expectedText: `
+    if (ctx.attr.cpu == "cpuA"):
+        tool_paths = []
+    elif (ctx.attr.cpu == "cpuC"):
+        tool_paths = [tool_path(name = "A", path = "a/b/c")]
+    else:
+        fail("Unreachable")`},
+		{field: "tool_paths",
+			toolchains: toolchainsSameNonempty,
+			expectedText: `
+    tool_paths = [tool_path(name = "A", path = "a/b/c")]`},
+		{field: "tool_paths",
+			toolchains: toolchainsDifferentOrder,
+			expectedText: `
+    if (ctx.attr.cpu == "cpuC"):
+        tool_paths = [
+            tool_path(name = "A", path = "a/b/c"),
+            tool_path(name = "B", path = "a/b/c"),
+        ]
+    elif (ctx.attr.cpu == "cpuD"):
+        tool_paths = [
+            tool_path(name = "B", path = "a/b/c"),
+            tool_path(name = "A", path = "a/b/c"),
+        ]
+    else:
+        fail("Unreachable")`},
+		{field: "tool_paths",
+			toolchains: allToolchains,
+			expectedText: `
+    if (ctx.attr.cpu == "cpuC" and ctx.attr.compiler == "compilerC"):
+        tool_paths = [
+            tool_path(name = "A", path = "a/b/c"),
+            tool_path(name = "B", path = "a/b/c"),
+        ]
+    elif (ctx.attr.cpu == "cpuD"):
+        tool_paths = [
+            tool_path(name = "B", path = "a/b/c"),
+            tool_path(name = "A", path = "a/b/c"),
+        ]
+    elif (ctx.attr.cpu == "cpuA"
+        or ctx.attr.cpu == "cpuB"):
+        tool_paths = []
+    elif (ctx.attr.cpu == "cpuC" and ctx.attr.compiler == "compilerA"
+        or ctx.attr.cpu == "cpuC" and ctx.attr.compiler == "compilerB"):
+        tool_paths = [tool_path(name = "A", path = "a/b/c")]
+    else:
+        fail("Unreachable")`}}
+
+	for _, tc := range testCases {
+		crosstool := makeCrosstool(tc.toolchains)
+		got, err := Transform(crosstool)
+		if err != nil {
+			t.Fatalf("CROSSTOOL conversion failed: %v", err)
+		}
+		if !strings.Contains(got, tc.expectedText) {
+			t.Errorf("Failed to correctly convert '%s' field, expected to contain:\n%v\n",
+				tc.field, tc.expectedText)
+			t.Fatalf("Tested CROSSTOOL:\n%v\n\nGenerated rule:\n%v\n",
+				strings.Join(tc.toolchains, "\n"), got)
+		}
+	}
+}
+
+func getArtifactNamePattern(lines []string) string {
+	return fmt.Sprintf(`artifact_name_pattern {
+  %s
+}`, strings.Join(lines, "\n  "))
+}
+
+func TestArtifactNamePatterns(t *testing.T) {
+	toolchainEmpty1 := getCToolchain("1", "cpuA", "compilerA", []string{})
+	toolchainEmpty2 := getCToolchain("2", "cpuB", "compilerA", []string{})
+	toolchainA1 := getCToolchain("3", "cpuC", "compilerA",
+		[]string{
+			getArtifactNamePattern([]string{
+				"category_name: 'A'",
+				"prefix: 'p'",
+				"extension: '.exe'"}),
+		},
+	)
+	toolchainA2 := getCToolchain("4", "cpuC", "compilerB",
+		[]string{
+			getArtifactNamePattern([]string{
+				"category_name: 'A'",
+				"prefix: 'p'",
+				"extension: '.exe'"}),
+		},
+	)
+	toolchainAB := getCToolchain("5", "cpuC", "compilerC",
+		[]string{
+			getArtifactNamePattern([]string{
+				"category_name: 'A'",
+				"prefix: 'p'",
+				"extension: '.exe'"}),
+			getArtifactNamePattern([]string{
+				"category_name: 'B'",
+				"prefix: 'p'",
+				"extension: '.exe'"}),
+		},
+	)
+	toolchainBA := getCToolchain("6", "cpuD", "compilerA",
+		[]string{
+			getArtifactNamePattern([]string{
+				"category_name: 'B'",
+				"prefix: 'p'",
+				"extension: '.exe'"}),
+			getArtifactNamePattern([]string{
+				"category_name: 'A'",
+				"prefix: 'p'",
+				"extension: '.exe'"}),
+		},
+	)
+	toolchainsEmpty := []string{toolchainEmpty1, toolchainEmpty2}
+
+	toolchainsOneNonempty := []string{toolchainEmpty1, toolchainA1}
+
+	toolchainsSameNonempty := []string{toolchainA1, toolchainA2}
+
+	toolchainsDifferentOrder := []string{toolchainAB, toolchainBA}
+
+	allToolchains := []string{
+		toolchainEmpty1,
+		toolchainEmpty2,
+		toolchainA1,
+		toolchainA2,
+		toolchainAB,
+		toolchainBA,
+	}
+
+	testCases := []struct {
+		field        string
+		toolchains   []string
+		expectedText string
+	}{
+		{field: "artifact_name_patterns",
+			toolchains: toolchainsEmpty,
+			expectedText: `
+    artifact_name_patterns = []`},
+		{field: "artifact_name_patterns",
+			toolchains: toolchainsOneNonempty,
+			expectedText: `
+    if (ctx.attr.cpu == "cpuC"):
+        artifact_name_patterns = [
+            artifact_name_pattern(
+                category_name = "A",
+                prefix = "p",
+                extension = ".exe",
+            ),
+        ]
+    elif (ctx.attr.cpu == "cpuA"):
+        artifact_name_patterns = []
+    else:
+        fail("Unreachable")`},
+		{field: "artifact_name_patterns",
+			toolchains: toolchainsSameNonempty,
+			expectedText: `
+    artifact_name_patterns = [
+        artifact_name_pattern(
+            category_name = "A",
+            prefix = "p",
+            extension = ".exe",
+        ),
+    ]`},
+		{field: "artifact_name_patterns",
+			toolchains: toolchainsDifferentOrder,
+			expectedText: `
+    if (ctx.attr.cpu == "cpuC"):
+        artifact_name_patterns = [
+            artifact_name_pattern(
+                category_name = "A",
+                prefix = "p",
+                extension = ".exe",
+            ),
+            artifact_name_pattern(
+                category_name = "B",
+                prefix = "p",
+                extension = ".exe",
+            ),
+        ]
+    elif (ctx.attr.cpu == "cpuD"):
+        artifact_name_patterns = [
+            artifact_name_pattern(
+                category_name = "B",
+                prefix = "p",
+                extension = ".exe",
+            ),
+            artifact_name_pattern(
+                category_name = "A",
+                prefix = "p",
+                extension = ".exe",
+            ),
+        ]
+    else:
+        fail("Unreachable")`},
+		{field: "artifact_name_patterns",
+			toolchains: allToolchains,
+			expectedText: `
+    if (ctx.attr.cpu == "cpuC" and ctx.attr.compiler == "compilerC"):
+        artifact_name_patterns = [
+            artifact_name_pattern(
+                category_name = "A",
+                prefix = "p",
+                extension = ".exe",
+            ),
+            artifact_name_pattern(
+                category_name = "B",
+                prefix = "p",
+                extension = ".exe",
+            ),
+        ]
+    elif (ctx.attr.cpu == "cpuC" and ctx.attr.compiler == "compilerA"
+        or ctx.attr.cpu == "cpuC" and ctx.attr.compiler == "compilerB"):
+        artifact_name_patterns = [
+            artifact_name_pattern(
+                category_name = "A",
+                prefix = "p",
+                extension = ".exe",
+            ),
+        ]
+    elif (ctx.attr.cpu == "cpuD"):
+        artifact_name_patterns = [
+            artifact_name_pattern(
+                category_name = "B",
+                prefix = "p",
+                extension = ".exe",
+            ),
+            artifact_name_pattern(
+                category_name = "A",
+                prefix = "p",
+                extension = ".exe",
+            ),
+        ]
+    elif (ctx.attr.cpu == "cpuA"
+        or ctx.attr.cpu == "cpuB"):
+        artifact_name_patterns = []
+    else:
+        fail("Unreachable")`}}
+
+	for _, tc := range testCases {
+		crosstool := makeCrosstool(tc.toolchains)
+		got, err := Transform(crosstool)
+		if err != nil {
+			t.Fatalf("CROSSTOOL conversion failed: %v", err)
+		}
+		if !strings.Contains(got, tc.expectedText) {
+			t.Errorf("Failed to correctly convert '%s' field, expected to contain:\n%v\n",
+				tc.field, tc.expectedText)
+			t.Fatalf("Tested CROSSTOOL:\n%v\n\nGenerated rule:\n%v\n",
+				strings.Join(tc.toolchains, "\n"), got)
+		}
+	}
+}
+
+func getFeature(lines []string) string {
+	return fmt.Sprintf(`feature {
+  %s
+}`, strings.Join(lines, "\n  "))
+}
+
+func TestFeatureListAssignment(t *testing.T) {
+	toolchainEmpty1 := getCToolchain("1", "cpuA", "compilerA", []string{})
+	toolchainEmpty2 := getCToolchain("2", "cpuB", "compilerA", []string{})
+	toolchainA1 := getCToolchain("3", "cpuC", "compilerA",
+		[]string{getFeature([]string{"name: 'A'"})},
+	)
+	toolchainA2 := getCToolchain("4", "cpuC", "compilerB",
+		[]string{getFeature([]string{"name: 'A'"})},
+	)
+	toolchainAB := getCToolchain("5", "cpuC", "compilerC",
+		[]string{
+			getFeature([]string{"name: 'A'"}),
+			getFeature([]string{"name: 'B'"}),
+		},
+	)
+	toolchainBA := getCToolchain("6", "cpuD", "compilerA",
+		[]string{
+			getFeature([]string{"name: 'B'"}),
+			getFeature([]string{"name: 'A'"}),
+		},
+	)
+	toolchainsEmpty := []string{toolchainEmpty1, toolchainEmpty2}
+
+	toolchainsOneNonempty := []string{toolchainEmpty1, toolchainA1}
+
+	toolchainsSameNonempty := []string{toolchainA1, toolchainA2}
+
+	toolchainsDifferentOrder := []string{toolchainAB, toolchainBA}
+
+	allToolchains := []string{
+		toolchainEmpty1,
+		toolchainEmpty2,
+		toolchainA1,
+		toolchainA2,
+		toolchainAB,
+		toolchainBA,
+	}
+
+	testCases := []struct {
+		field        string
+		toolchains   []string
+		expectedText string
+	}{
+		{field: "features",
+			toolchains: toolchainsEmpty,
+			expectedText: `
+    features = []`},
+		{field: "features",
+			toolchains: toolchainsOneNonempty,
+			expectedText: `
+    if (ctx.attr.cpu == "cpuA"):
+        features = []
+    elif (ctx.attr.cpu == "cpuC"):
+        features = [a_feature]
+    else:
+        fail("Unreachable")`},
+		{field: "features",
+			toolchains: toolchainsSameNonempty,
+			expectedText: `
+    features = [a_feature]`},
+		{field: "features",
+			toolchains: toolchainsDifferentOrder,
+			expectedText: `
+    if (ctx.attr.cpu == "cpuC"):
+        features = [a_feature, b_feature]
+    elif (ctx.attr.cpu == "cpuD"):
+        features = [b_feature, a_feature]
+    else:
+        fail("Unreachable")`},
+		{field: "features",
+			toolchains: allToolchains,
+			expectedText: `
+    if (ctx.attr.cpu == "cpuA"
+        or ctx.attr.cpu == "cpuB"):
+        features = []
+    elif (ctx.attr.cpu == "cpuC" and ctx.attr.compiler == "compilerA"
+        or ctx.attr.cpu == "cpuC" and ctx.attr.compiler == "compilerB"):
+        features = [a_feature]
+    elif (ctx.attr.cpu == "cpuC" and ctx.attr.compiler == "compilerC"):
+        features = [a_feature, b_feature]
+    elif (ctx.attr.cpu == "cpuD"):
+        features = [b_feature, a_feature]
+    else:
+        fail("Unreachable")`}}
+
+	for _, tc := range testCases {
+		crosstool := makeCrosstool(tc.toolchains)
+		got, err := Transform(crosstool)
+		if err != nil {
+			t.Fatalf("CROSSTOOL conversion failed: %v", err)
+		}
+		if !strings.Contains(got, tc.expectedText) {
+			t.Errorf("Failed to correctly convert '%s' field, expected to contain:\n%v\n",
+				tc.field, tc.expectedText)
+			t.Fatalf("Tested CROSSTOOL:\n%v\n\nGenerated rule:\n%v\n",
+				strings.Join(tc.toolchains, "\n"), got)
+		}
+	}
+}
+
+func getActionConfig(lines []string) string {
+	return fmt.Sprintf(`action_config {
+  %s
+}`, strings.Join(lines, "\n  "))
+}
+
+func TestActionConfigListAssignment(t *testing.T) {
+	toolchainEmpty1 := getCToolchain("1", "cpuA", "compilerA", []string{})
+	toolchainEmpty2 := getCToolchain("2", "cpuB", "compilerA", []string{})
+	toolchainA1 := getCToolchain("3", "cpuC", "compilerA",
+		[]string{
+			getActionConfig([]string{"action_name: 'A'", "config_name: 'A'"}),
+		},
+	)
+	toolchainA2 := getCToolchain("4", "cpuC", "compilerB",
+		[]string{
+			getActionConfig([]string{"action_name: 'A'", "config_name: 'A'"}),
+		},
+	)
+	toolchainAB := getCToolchain("5", "cpuC", "compilerC",
+		[]string{
+			getActionConfig([]string{"action_name: 'A'", "config_name: 'A'"}),
+			getActionConfig([]string{"action_name: 'B'", "config_name: 'B'"}),
+		},
+	)
+	toolchainBA := getCToolchain("6", "cpuD", "compilerA",
+		[]string{
+			getActionConfig([]string{"action_name: 'B'", "config_name: 'B'"}),
+			getActionConfig([]string{"action_name: 'A'", "config_name: 'A'"}),
+		},
+	)
+	toolchainsEmpty := []string{toolchainEmpty1, toolchainEmpty2}
+
+	toolchainsOneNonempty := []string{toolchainEmpty1, toolchainA1}
+
+	toolchainsSameNonempty := []string{toolchainA1, toolchainA2}
+
+	toolchainsDifferentOrder := []string{toolchainAB, toolchainBA}
+
+	allToolchains := []string{
+		toolchainEmpty1,
+		toolchainEmpty2,
+		toolchainA1,
+		toolchainA2,
+		toolchainAB,
+		toolchainBA,
+	}
+
+	testCases := []struct {
+		field        string
+		toolchains   []string
+		expectedText string
+	}{
+		{field: "action_configs",
+			toolchains: toolchainsEmpty,
+			expectedText: `
+    action_configs = []`},
+		{field: "action_configs",
+			toolchains: toolchainsOneNonempty,
+			expectedText: `
+    if (ctx.attr.cpu == "cpuA"):
+        action_configs = []
+    elif (ctx.attr.cpu == "cpuC"):
+        action_configs = [a_action]
+    else:
+        fail("Unreachable")`},
+		{field: "action_configs",
+			toolchains: toolchainsSameNonempty,
+			expectedText: `
+    action_configs = [a_action]`},
+		{field: "action_configs",
+			toolchains: toolchainsDifferentOrder,
+			expectedText: `
+    if (ctx.attr.cpu == "cpuC"):
+        action_configs = [a_action, b_action]
+    elif (ctx.attr.cpu == "cpuD"):
+        action_configs = [b_action, a_action]
+    else:
+        fail("Unreachable")`},
+		{field: "action_configs",
+			toolchains: allToolchains,
+			expectedText: `
+    if (ctx.attr.cpu == "cpuA"
+        or ctx.attr.cpu == "cpuB"):
+        action_configs = []
+    elif (ctx.attr.cpu == "cpuC" and ctx.attr.compiler == "compilerA"
+        or ctx.attr.cpu == "cpuC" and ctx.attr.compiler == "compilerB"):
+        action_configs = [a_action]
+    elif (ctx.attr.cpu == "cpuC" and ctx.attr.compiler == "compilerC"):
+        action_configs = [a_action, b_action]
+    elif (ctx.attr.cpu == "cpuD"):
+        action_configs = [b_action, a_action]
+    else:
+        fail("Unreachable")`}}
+
+	for _, tc := range testCases {
+		crosstool := makeCrosstool(tc.toolchains)
+		got, err := Transform(crosstool)
+		if err != nil {
+			t.Fatalf("CROSSTOOL conversion failed: %v", err)
+		}
+		if !strings.Contains(got, tc.expectedText) {
+			t.Errorf("Failed to correctly convert '%s' field, expected to contain:\n%v\n",
+				tc.field, tc.expectedText)
+			t.Fatalf("Tested CROSSTOOL:\n%v\n\nGenerated rule:\n%v\n",
+				strings.Join(tc.toolchains, "\n"), got)
+		}
+	}
+}
+
+func TestFeatureAssignment(t *testing.T) {
+	toolchainEmpty1 := getCToolchain("1", "cpuA", "compilerA", []string{})
+	toolchainEmpty2 := getCToolchain("2", "cpuB", "compilerA", []string{})
+	toolchainA1 := getCToolchain("3", "cpuC", "compilerA",
+		[]string{
+			getActionConfig([]string{"action_name: 'A'", "config_name: 'A'"}),
+		},
+	)
+	toolchainA2 := getCToolchain("4", "cpuC", "compilerB",
+		[]string{
+			getActionConfig([]string{"action_name: 'A'", "config_name: 'A'"}),
+		},
+	)
+	toolchainAB := getCToolchain("5", "cpuC", "compilerC",
+		[]string{
+			getActionConfig([]string{"action_name: 'A'", "config_name: 'A'"}),
+			getActionConfig([]string{"action_name: 'B'", "config_name: 'B'"}),
+		},
+	)
+	toolchainBA := getCToolchain("6", "cpuD", "compilerA",
+		[]string{
+			getActionConfig([]string{"action_name: 'B'", "config_name: 'B'"}),
+			getActionConfig([]string{"action_name: 'A'", "config_name: 'A'"}),
+		},
+	)
+	toolchainsEmpty := []string{toolchainEmpty1, toolchainEmpty2}
+
+	toolchainsOneNonempty := []string{toolchainEmpty1, toolchainA1}
+
+	toolchainsSameNonempty := []string{toolchainA1, toolchainA2}
+
+	toolchainsDifferentOrder := []string{toolchainAB, toolchainBA}
+
+	allToolchains := []string{
+		toolchainEmpty1,
+		toolchainEmpty2,
+		toolchainA1,
+		toolchainA2,
+		toolchainAB,
+		toolchainBA,
+	}
+
+	testCases := []struct {
+		field        string
+		toolchains   []string
+		expectedText string
+	}{
+		{field: "action_configs",
+			toolchains: toolchainsEmpty,
+			expectedText: `
+    action_configs = []`},
+		{field: "action_configs",
+			toolchains: toolchainsOneNonempty,
+			expectedText: `
+    if (ctx.attr.cpu == "cpuA"):
+        action_configs = []
+    elif (ctx.attr.cpu == "cpuC"):
+        action_configs = [a_action]
+    else:
+        fail("Unreachable")`},
+		{field: "action_configs",
+			toolchains: toolchainsSameNonempty,
+			expectedText: `
+    action_configs = [a_action]`},
+		{field: "action_configs",
+			toolchains: toolchainsDifferentOrder,
+			expectedText: `
+    if (ctx.attr.cpu == "cpuC"):
+        action_configs = [a_action, b_action]
+    elif (ctx.attr.cpu == "cpuD"):
+        action_configs = [b_action, a_action]
+    else:
+        fail("Unreachable")`},
+		{field: "action_configs",
+			toolchains: allToolchains,
+			expectedText: `
+    if (ctx.attr.cpu == "cpuA"
+        or ctx.attr.cpu == "cpuB"):
+        action_configs = []
+    elif (ctx.attr.cpu == "cpuC" and ctx.attr.compiler == "compilerA"
+        or ctx.attr.cpu == "cpuC" and ctx.attr.compiler == "compilerB"):
+        action_configs = [a_action]
+    elif (ctx.attr.cpu == "cpuC" and ctx.attr.compiler == "compilerC"):
+        action_configs = [a_action, b_action]
+    elif (ctx.attr.cpu == "cpuD"):
+        action_configs = [b_action, a_action]
+    else:
+        fail("Unreachable")`}}
+
+	for _, tc := range testCases {
+		crosstool := makeCrosstool(tc.toolchains)
+		got, err := Transform(crosstool)
+		if err != nil {
+			t.Fatalf("CROSSTOOL conversion failed: %v", err)
+		}
+		if !strings.Contains(got, tc.expectedText) {
+			t.Errorf("Failed to correctly convert '%s' field, expected to contain:\n%v\n",
+				tc.field, tc.expectedText)
+			t.Fatalf("Tested CROSSTOOL:\n%v\n\nGenerated rule:\n%v\n",
+				strings.Join(tc.toolchains, "\n"), got)
+		}
+	}
+}