Home | History | Annotate | Download | only in bots
      1 // Copyright 2016 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 package main
      6 
      7 /*
      8 	Generate the tasks.json file.
      9 */
     10 
     11 import (
     12 	"encoding/json"
     13 	"flag"
     14 	"fmt"
     15 	"io/ioutil"
     16 	"os"
     17 	"path"
     18 	"path/filepath"
     19 	"regexp"
     20 	"runtime"
     21 	"sort"
     22 	"strconv"
     23 	"strings"
     24 	"time"
     25 
     26 	"github.com/skia-dev/glog"
     27 	"go.skia.org/infra/go/sklog"
     28 	"go.skia.org/infra/go/util"
     29 	"go.skia.org/infra/task_scheduler/go/specs"
     30 )
     31 
     32 const (
     33 	BUNDLE_RECIPES_NAME         = "Housekeeper-PerCommit-BundleRecipes"
     34 	ISOLATE_SKIMAGE_NAME        = "Housekeeper-PerCommit-IsolateSkImage"
     35 	ISOLATE_SKP_NAME            = "Housekeeper-PerCommit-IsolateSKP"
     36 	ISOLATE_SVG_NAME            = "Housekeeper-PerCommit-IsolateSVG"
     37 	ISOLATE_NDK_LINUX_NAME      = "Housekeeper-PerCommit-IsolateAndroidNDKLinux"
     38 	ISOLATE_SDK_LINUX_NAME      = "Housekeeper-PerCommit-IsolateAndroidSDKLinux"
     39 	ISOLATE_WIN_TOOLCHAIN_NAME  = "Housekeeper-PerCommit-IsolateWinToolchain"
     40 	ISOLATE_WIN_VULKAN_SDK_NAME = "Housekeeper-PerCommit-IsolateWinVulkanSDK"
     41 
     42 	DEFAULT_OS_DEBIAN    = "Debian-9.1"
     43 	DEFAULT_OS_LINUX_GCE = "Debian-9.2"
     44 	DEFAULT_OS_MAC       = "Mac-10.13.3"
     45 	DEFAULT_OS_UBUNTU    = "Ubuntu-14.04"
     46 	DEFAULT_OS_WIN       = "Windows-2016Server-14393"
     47 
     48 	// Name prefix for upload jobs.
     49 	PREFIX_UPLOAD = "Upload"
     50 )
     51 
     52 var (
     53 	// "Constants"
     54 
     55 	// Top-level list of all jobs to run at each commit; loaded from
     56 	// jobs.json.
     57 	JOBS []string
     58 
     59 	// General configuration information.
     60 	CONFIG struct {
     61 		GsBucketCoverage string   `json:"gs_bucket_coverage"`
     62 		GsBucketGm       string   `json:"gs_bucket_gm"`
     63 		GsBucketNano     string   `json:"gs_bucket_nano"`
     64 		GsBucketCalm     string   `json:"gs_bucket_calm"`
     65 		NoUpload         []string `json:"no_upload"`
     66 		Pool             string   `json:"pool"`
     67 	}
     68 
     69 	// alternateSwarmDimensions can be set in an init function to override the default swarming bot
     70 	// dimensions for the given task.
     71 	alternateSwarmDimensions func(parts map[string]string) []string
     72 
     73 	// internalHardwareLabelFn can be set in an init function to provide an
     74 	// internal_hardware_label variable to the recipe.
     75 	internalHardwareLabelFn func(parts map[string]string) *int
     76 
     77 	// Defines the structure of job names.
     78 	jobNameSchema *JobNameSchema
     79 
     80 	// Git 2.13.
     81 	cipdGit1 = &specs.CipdPackage{
     82 		Name:    fmt.Sprintf("infra/git/${platform}"),
     83 		Path:    "git",
     84 		Version: fmt.Sprintf("version:2.13.0.chromium9"),
     85 	}
     86 	cipdGit2 = &specs.CipdPackage{
     87 		Name:    fmt.Sprintf("infra/tools/git/${platform}"),
     88 		Path:    "git",
     89 		Version: fmt.Sprintf("git_revision:a78b5f3658c0578a017db48df97d20ac09822bcd"),
     90 	}
     91 
     92 	// Flags.
     93 	builderNameSchemaFile = flag.String("builder_name_schema", "", "Path to the builder_name_schema.json file. If not specified, uses infra/bots/recipe_modules/builder_name_schema/builder_name_schema.json from this repo.")
     94 	assetsDir             = flag.String("assets_dir", "", "Directory containing assets.")
     95 	cfgFile               = flag.String("cfg_file", "", "JSON file containing general configuration information.")
     96 	jobsFile              = flag.String("jobs", "", "JSON file containing jobs to run.")
     97 )
     98 
     99 // internalHardwareLabel returns the internal ID for the bot, if any.
    100 func internalHardwareLabel(parts map[string]string) *int {
    101 	if internalHardwareLabelFn != nil {
    102 		return internalHardwareLabelFn(parts)
    103 	}
    104 	return nil
    105 }
    106 
    107 // linuxGceDimensions are the Swarming dimensions for Linux GCE
    108 // instances.
    109 func linuxGceDimensions() []string {
    110 	return []string{
    111 		// Specify CPU to avoid running builds on bots with a more unique CPU.
    112 		"cpu:x86-64-Haswell_GCE",
    113 		"gpu:none",
    114 		fmt.Sprintf("os:%s", DEFAULT_OS_LINUX_GCE),
    115 		fmt.Sprintf("pool:%s", CONFIG.Pool),
    116 	}
    117 }
    118 
    119 // deriveCompileTaskName returns the name of a compile task based on the given
    120 // job name.
    121 func deriveCompileTaskName(jobName string, parts map[string]string) string {
    122 	if strings.Contains(jobName, "Bookmaker") {
    123 		return "Build-Debian9-GCC-x86_64-Release"
    124 	} else if parts["role"] == "Housekeeper" {
    125 		return "Build-Debian9-GCC-x86_64-Release-Shared"
    126 	} else if parts["role"] == "Test" || parts["role"] == "Perf" || parts["role"] == "Calmbench" {
    127 		task_os := parts["os"]
    128 		ec := []string{}
    129 		if val := parts["extra_config"]; val != "" {
    130 			ec = strings.Split(val, "_")
    131 			ignore := []string{"Skpbench", "AbandonGpuContext", "PreAbandonGpuContext", "Valgrind", "ReleaseAndAbandonGpuContext", "CCPR", "FSAA", "FAAA", "FDAA", "NativeFonts", "GDI", "NoGPUThreads"}
    132 			keep := make([]string, 0, len(ec))
    133 			for _, part := range ec {
    134 				if !util.In(part, ignore) {
    135 					keep = append(keep, part)
    136 				}
    137 			}
    138 			ec = keep
    139 		}
    140 		if task_os == "Android" {
    141 			if !util.In("Android", ec) {
    142 				ec = append([]string{"Android"}, ec...)
    143 			}
    144 			task_os = "Debian9"
    145 		} else if task_os == "Chromecast" {
    146 			task_os = "Debian9"
    147 			ec = append([]string{"Chromecast"}, ec...)
    148 		} else if strings.Contains(task_os, "ChromeOS") {
    149 			ec = append([]string{"Chromebook", "GLES"}, ec...)
    150 			task_os = "Debian9"
    151 		} else if task_os == "iOS" {
    152 			ec = append([]string{task_os}, ec...)
    153 			task_os = "Mac"
    154 		} else if strings.Contains(task_os, "Win") {
    155 			task_os = "Win"
    156 		} else if strings.Contains(task_os, "Ubuntu") || strings.Contains(task_os, "Debian") {
    157 			task_os = "Debian9"
    158 		}
    159 		jobNameMap := map[string]string{
    160 			"role":          "Build",
    161 			"os":            task_os,
    162 			"compiler":      parts["compiler"],
    163 			"target_arch":   parts["arch"],
    164 			"configuration": parts["configuration"],
    165 		}
    166 		if len(ec) > 0 {
    167 			jobNameMap["extra_config"] = strings.Join(ec, "_")
    168 		}
    169 		name, err := jobNameSchema.MakeJobName(jobNameMap)
    170 		if err != nil {
    171 			glog.Fatal(err)
    172 		}
    173 		return name
    174 	} else {
    175 		return jobName
    176 	}
    177 }
    178 
    179 // swarmDimensions generates swarming bot dimensions for the given task.
    180 func swarmDimensions(parts map[string]string) []string {
    181 	if alternateSwarmDimensions != nil {
    182 		return alternateSwarmDimensions(parts)
    183 	}
    184 	return defaultSwarmDimensions(parts)
    185 }
    186 
    187 // defaultSwarmDimensions generates default swarming bot dimensions for the given task.
    188 func defaultSwarmDimensions(parts map[string]string) []string {
    189 	d := map[string]string{
    190 		"pool": CONFIG.Pool,
    191 	}
    192 	if os, ok := parts["os"]; ok {
    193 		d["os"], ok = map[string]string{
    194 			"Android":    "Android",
    195 			"Chromecast": "Android",
    196 			"ChromeOS":   "ChromeOS",
    197 			"Debian9":    DEFAULT_OS_DEBIAN,
    198 			"Mac":        DEFAULT_OS_MAC,
    199 			"Ubuntu14":   DEFAULT_OS_UBUNTU,
    200 			"Ubuntu16":   "Ubuntu-16.10",
    201 			"Ubuntu17":   "Ubuntu-17.04",
    202 			"Win":        DEFAULT_OS_WIN,
    203 			"Win10":      "Windows-10-16299.248",
    204 			"Win2k8":     "Windows-2008ServerR2-SP1",
    205 			"Win2016":    DEFAULT_OS_WIN,
    206 			"Win7":       "Windows-7-SP1",
    207 			"Win8":       "Windows-8.1-SP0",
    208 			"iOS":        "iOS-10.3.1",
    209 		}[os]
    210 		if !ok {
    211 			glog.Fatalf("Entry %q not found in OS mapping.", os)
    212 		}
    213 		if os == "Win10" && parts["model"] == "Golo" {
    214 			// Golo/MTV lab bots have Windows 10 version 1703, whereas Skolo bots have Windows 10 version
    215 			// 1709.
    216 			d["os"] = "Windows-10-15063"
    217 		}
    218 	} else {
    219 		d["os"] = DEFAULT_OS_DEBIAN
    220 	}
    221 	if parts["role"] == "Test" || parts["role"] == "Perf" || parts["role"] == "Calmbench" {
    222 		if strings.Contains(parts["os"], "Android") || strings.Contains(parts["os"], "Chromecast") {
    223 			// For Android, the device type is a better dimension
    224 			// than CPU or GPU.
    225 			deviceInfo, ok := map[string][]string{
    226 				"AndroidOne":      {"sprout", "MOB30Q"},
    227 				"Chorizo":         {"chorizo", "1.30_109591"},
    228 				"GalaxyS6":        {"zerofltetmo", "NRD90M_G920TUVU5FQK1"},
    229 				"GalaxyS7_G930A":  {"heroqlteatt", "NRD90M_G930AUCS4BQC2"},
    230 				"GalaxyS7_G930FD": {"herolte", "NRD90M_G930FXXU1DQAS"},
    231 				"MotoG4":          {"athene", "NPJ25.93-14"},
    232 				"NVIDIA_Shield":   {"foster", "NRD90M_1915764_848"},
    233 				"Nexus5":          {"hammerhead", "M4B30Z_3437181"},
    234 				"Nexus5x":         {"bullhead", "OPR6.170623.023"},
    235 				"Nexus7":          {"grouper", "LMY47V_1836172"}, // 2012 Nexus 7
    236 				"NexusPlayer":     {"fugu", "OPR6.170623.021"},
    237 				"Pixel":           {"sailfish", "OPM1.171019.016"},
    238 				"Pixel2XL":        {"taimen", "OPD1.170816.023"},
    239 				"PixelC":          {"dragon", "OPR1.170623.034"},
    240 			}[parts["model"]]
    241 			if !ok {
    242 				glog.Fatalf("Entry %q not found in Android mapping.", parts["model"])
    243 			}
    244 			d["device_type"] = deviceInfo[0]
    245 			d["device_os"] = deviceInfo[1]
    246 			// TODO(kjlubick): Remove the python dimension after we have removed the
    247 			// Nexus5x devices from the local lab (on Monday, Dec 11, 2017 should be fine).
    248 			d["python"] = "2.7.9" // This indicates a RPI, e.g. in Skolo.  Golo is 2.7.12
    249 			if parts["model"] == "Nexus5x" {
    250 				d["python"] = "2.7.12"
    251 			}
    252 		} else if strings.Contains(parts["os"], "iOS") {
    253 			device, ok := map[string]string{
    254 				"iPadMini4": "iPad5,1",
    255 				"iPhone6":   "iPhone7,2",
    256 				"iPhone7":   "iPhone9,1",
    257 				"iPadPro":   "iPad6,3",
    258 			}[parts["model"]]
    259 			if !ok {
    260 				glog.Fatalf("Entry %q not found in iOS mapping.", parts["model"])
    261 			}
    262 			d["device"] = device
    263 		} else if parts["cpu_or_gpu"] == "CPU" {
    264 			modelMapping, ok := map[string]map[string]string{
    265 				"AVX": {
    266 					"MacMini7.1": "x86-64-E5-2697_v2",
    267 					"Golo":       "x86-64-E5-2670",
    268 				},
    269 				"AVX2": {
    270 					"GCE":       "x86-64-Haswell_GCE",
    271 					"NUC5i7RYH": "x86-64-i7-5557U",
    272 				},
    273 				"AVX512": {
    274 					"GCE": "x86-64-Skylake_GCE",
    275 				},
    276 			}[parts["cpu_or_gpu_value"]]
    277 			if !ok {
    278 				glog.Fatalf("Entry %q not found in CPU mapping.", parts["cpu_or_gpu_value"])
    279 			}
    280 			cpu, ok := modelMapping[parts["model"]]
    281 			if !ok {
    282 				glog.Fatalf("Entry %q not found in %q model mapping.", parts["model"], parts["cpu_or_gpu_value"])
    283 			}
    284 			d["cpu"] = cpu
    285 			if parts["model"] == "GCE" && d["os"] == DEFAULT_OS_DEBIAN {
    286 				d["os"] = DEFAULT_OS_LINUX_GCE
    287 			}
    288 			if parts["model"] == "GCE" && d["os"] == DEFAULT_OS_WIN {
    289 				// Use normal-size machines for Test and Perf tasks on Win GCE.
    290 				d["machine_type"] = "n1-standard-16"
    291 			}
    292 		} else {
    293 			if strings.Contains(parts["os"], "Win") {
    294 				gpu, ok := map[string]string{
    295 					"GT610":         "10de:104a-22.21.13.8205",
    296 					"GTX1070":       "10de:1ba1-23.21.13.9101",
    297 					"GTX660":        "10de:11c0-23.21.13.9101",
    298 					"GTX960":        "10de:1401-23.21.13.9101",
    299 					"IntelHD4400":   "8086:0a16-20.19.15.4835",
    300 					"IntelIris540":  "8086:1926-21.20.16.4590",
    301 					"IntelIris6100": "8086:162b-20.19.15.4835",
    302 					"RadeonHD7770":  "1002:683d-23.20.15017.4003",
    303 					"RadeonR9M470X": "1002:6646-23.20.15017.4003",
    304 					"QuadroP400":    "10de:1cb3-22.21.13.8205",
    305 				}[parts["cpu_or_gpu_value"]]
    306 				if !ok {
    307 					glog.Fatalf("Entry %q not found in Win GPU mapping.", parts["cpu_or_gpu_value"])
    308 				}
    309 				d["gpu"] = gpu
    310 
    311 				// Specify cpu dimension for NUCs and ShuttleCs. We temporarily have two
    312 				// types of machines with a GTX960.
    313 				cpu, ok := map[string]string{
    314 					"NUC6i7KYK": "x86-64-i7-6770HQ",
    315 					"ShuttleC":  "x86-64-i7-6700K",
    316 				}[parts["model"]]
    317 				if ok {
    318 					d["cpu"] = cpu
    319 				}
    320 			} else if strings.Contains(parts["os"], "Ubuntu") || strings.Contains(parts["os"], "Debian") {
    321 				gpu, ok := map[string]string{
    322 					// Intel drivers come from CIPD, so no need to specify the version here.
    323 					"IntelBayTrail": "8086:0f31",
    324 					"IntelHD2000":   "8086:0102",
    325 					"IntelHD405":    "8086:22b1",
    326 					"IntelIris640":  "8086:5926",
    327 					"QuadroP400":    "10de:1cb3-384.59",
    328 				}[parts["cpu_or_gpu_value"]]
    329 				if !ok {
    330 					glog.Fatalf("Entry %q not found in Ubuntu GPU mapping.", parts["cpu_or_gpu_value"])
    331 				}
    332 				d["gpu"] = gpu
    333 			} else if strings.Contains(parts["os"], "Mac") {
    334 				gpu, ok := map[string]string{
    335 					"IntelHD6000":   "8086:1626",
    336 					"IntelHD615":    "8086:591e",
    337 					"IntelIris5100": "8086:0a2e",
    338 				}[parts["cpu_or_gpu_value"]]
    339 				if !ok {
    340 					glog.Fatalf("Entry %q not found in Mac GPU mapping.", parts["cpu_or_gpu_value"])
    341 				}
    342 				d["gpu"] = gpu
    343 				// Yuck. We have two different types of MacMini7,1 with the same GPU but different CPUs.
    344 				if parts["cpu_or_gpu_value"] == "IntelIris5100" {
    345 					// Run all tasks on Golo machines for now.
    346 					d["cpu"] = "x86-64-i7-4578U"
    347 				}
    348 			} else if strings.Contains(parts["os"], "ChromeOS") {
    349 				version, ok := map[string]string{
    350 					"MaliT604":           "9901.12.0",
    351 					"MaliT764":           "10172.0.0",
    352 					"MaliT860":           "10172.0.0",
    353 					"PowerVRGX6250":      "10176.5.0",
    354 					"TegraK1":            "10172.0.0",
    355 					"IntelHDGraphics615": "10032.17.0",
    356 				}[parts["cpu_or_gpu_value"]]
    357 				if !ok {
    358 					glog.Fatalf("Entry %q not found in ChromeOS GPU mapping.", parts["cpu_or_gpu_value"])
    359 				}
    360 				d["gpu"] = parts["cpu_or_gpu_value"]
    361 				d["release_version"] = version
    362 			} else {
    363 				glog.Fatalf("Unknown GPU mapping for OS %q.", parts["os"])
    364 			}
    365 		}
    366 	} else {
    367 		d["gpu"] = "none"
    368 		if d["os"] == DEFAULT_OS_DEBIAN {
    369 			return linuxGceDimensions()
    370 		} else if d["os"] == DEFAULT_OS_WIN {
    371 			// Windows CPU bots.
    372 			d["cpu"] = "x86-64-Haswell_GCE"
    373 			// Use many-core machines for Build tasks on Win GCE, except for Goma.
    374 			if strings.Contains(parts["extra_config"], "Goma") {
    375 				d["machine_type"] = "n1-standard-16"
    376 			} else {
    377 				d["machine_type"] = "n1-highcpu-64"
    378 			}
    379 		} else if d["os"] == DEFAULT_OS_MAC {
    380 			// Mac CPU bots.
    381 			d["cpu"] = "x86-64-E5-2697_v2"
    382 		}
    383 	}
    384 
    385 	rv := make([]string, 0, len(d))
    386 	for k, v := range d {
    387 		rv = append(rv, fmt.Sprintf("%s:%s", k, v))
    388 	}
    389 	sort.Strings(rv)
    390 	return rv
    391 }
    392 
    393 // relpath returns the relative path to the given file from the config file.
    394 func relpath(f string) string {
    395 	_, filename, _, _ := runtime.Caller(0)
    396 	dir := path.Dir(filename)
    397 	rel := dir
    398 	if *cfgFile != "" {
    399 		rel = path.Dir(*cfgFile)
    400 	}
    401 	rv, err := filepath.Rel(rel, path.Join(dir, f))
    402 	if err != nil {
    403 		sklog.Fatal(err)
    404 	}
    405 	return rv
    406 }
    407 
    408 // bundleRecipes generates the task to bundle and isolate the recipes.
    409 func bundleRecipes(b *specs.TasksCfgBuilder) string {
    410 	b.MustAddTask(BUNDLE_RECIPES_NAME, &specs.TaskSpec{
    411 		CipdPackages: []*specs.CipdPackage{cipdGit1, cipdGit2},
    412 		Dimensions:   linuxGceDimensions(),
    413 		ExtraArgs: []string{
    414 			"--workdir", "../../..", "bundle_recipes",
    415 			fmt.Sprintf("buildername=%s", BUNDLE_RECIPES_NAME),
    416 			fmt.Sprintf("swarm_out_dir=%s", specs.PLACEHOLDER_ISOLATED_OUTDIR),
    417 		},
    418 		Isolate:  relpath("bundle_recipes.isolate"),
    419 		Priority: 0.7,
    420 	})
    421 	return BUNDLE_RECIPES_NAME
    422 }
    423 
    424 // useBundledRecipes returns true iff the given bot should use bundled recipes
    425 // instead of syncing recipe DEPS itself.
    426 func useBundledRecipes(parts map[string]string) bool {
    427 	// Use bundled recipes for all test/perf tasks.
    428 	return true
    429 }
    430 
    431 type isolateAssetCfg struct {
    432 	isolateFile string
    433 	cipdPkg     string
    434 }
    435 
    436 var ISOLATE_ASSET_MAPPING = map[string]isolateAssetCfg{
    437 	ISOLATE_SKIMAGE_NAME: {
    438 		isolateFile: "isolate_skimage.isolate",
    439 		cipdPkg:     "skimage",
    440 	},
    441 	ISOLATE_SKP_NAME: {
    442 		isolateFile: "isolate_skp.isolate",
    443 		cipdPkg:     "skp",
    444 	},
    445 	ISOLATE_SVG_NAME: {
    446 		isolateFile: "isolate_svg.isolate",
    447 		cipdPkg:     "svg",
    448 	},
    449 	ISOLATE_NDK_LINUX_NAME: {
    450 		isolateFile: "isolate_ndk_linux.isolate",
    451 		cipdPkg:     "android_ndk_linux",
    452 	},
    453 	ISOLATE_SDK_LINUX_NAME: {
    454 		isolateFile: "isolate_android_sdk_linux.isolate",
    455 		cipdPkg:     "android_sdk_linux",
    456 	},
    457 	ISOLATE_WIN_TOOLCHAIN_NAME: {
    458 		isolateFile: "isolate_win_toolchain.isolate",
    459 		cipdPkg:     "win_toolchain",
    460 	},
    461 	ISOLATE_WIN_VULKAN_SDK_NAME: {
    462 		isolateFile: "isolate_win_vulkan_sdk.isolate",
    463 		cipdPkg:     "win_vulkan_sdk",
    464 	},
    465 }
    466 
    467 // bundleRecipes generates the task to bundle and isolate the recipes.
    468 func isolateCIPDAsset(b *specs.TasksCfgBuilder, name string) string {
    469 	b.MustAddTask(name, &specs.TaskSpec{
    470 		CipdPackages: []*specs.CipdPackage{
    471 			b.MustGetCipdPackageFromAsset(ISOLATE_ASSET_MAPPING[name].cipdPkg),
    472 		},
    473 		Dimensions: linuxGceDimensions(),
    474 		Isolate:    relpath(ISOLATE_ASSET_MAPPING[name].isolateFile),
    475 		Priority:   0.7,
    476 	})
    477 	return name
    478 }
    479 
    480 // getIsolatedCIPDDeps returns the slice of Isolate_* tasks a given task needs.
    481 // This allows us to  save time on I/O bound bots, like the RPIs.
    482 func getIsolatedCIPDDeps(parts map[string]string) []string {
    483 	deps := []string{}
    484 	// Only do this on the RPIs for now. Other, faster machines shouldn't see much
    485 	// benefit and we don't need the extra complexity, for now
    486 	rpiOS := []string{"Android", "ChromeOS", "iOS"}
    487 
    488 	if o := parts["os"]; strings.Contains(o, "Chromecast") {
    489 		// Chromecasts don't have enough disk space to fit all of the content,
    490 		// so we do a subset of the skps.
    491 		deps = append(deps, ISOLATE_SKP_NAME)
    492 	} else if e := parts["extra_config"]; strings.Contains(e, "Skpbench") {
    493 		// Skpbench only needs skps
    494 		deps = append(deps, ISOLATE_SKP_NAME)
    495 	} else if util.In(o, rpiOS) {
    496 		deps = append(deps, ISOLATE_SKP_NAME)
    497 		deps = append(deps, ISOLATE_SVG_NAME)
    498 		deps = append(deps, ISOLATE_SKIMAGE_NAME)
    499 	}
    500 
    501 	return deps
    502 }
    503 
    504 // compile generates a compile task. Returns the name of the last task in the
    505 // generated chain of tasks, which the Job should add as a dependency.
    506 func compile(b *specs.TasksCfgBuilder, name string, parts map[string]string) string {
    507 	// Collect the necessary CIPD packages.
    508 	pkgs := []*specs.CipdPackage{}
    509 	deps := []string{}
    510 
    511 	// Android bots require a toolchain.
    512 	if strings.Contains(name, "Android") {
    513 		if parts["extra_config"] == "Android_Framework" {
    514 			// Do not need a toolchain when building the
    515 			// Android Framework.
    516 		} else if strings.Contains(name, "Mac") {
    517 			pkgs = append(pkgs, b.MustGetCipdPackageFromAsset("android_ndk_darwin"))
    518 		} else if strings.Contains(name, "Win") {
    519 			pkg := b.MustGetCipdPackageFromAsset("android_ndk_windows")
    520 			pkg.Path = "n"
    521 			pkgs = append(pkgs, pkg)
    522 		} else {
    523 			deps = append(deps, isolateCIPDAsset(b, ISOLATE_NDK_LINUX_NAME))
    524 		}
    525 	} else if strings.Contains(name, "Chromecast") {
    526 		pkgs = append(pkgs, b.MustGetCipdPackageFromAsset("cast_toolchain"))
    527 		pkgs = append(pkgs, b.MustGetCipdPackageFromAsset("chromebook_arm_gles"))
    528 	} else if strings.Contains(name, "Chromebook") {
    529 		pkgs = append(pkgs, b.MustGetCipdPackageFromAsset("clang_linux"))
    530 		if parts["target_arch"] == "x86_64" {
    531 			pkgs = append(pkgs, b.MustGetCipdPackageFromAsset("chromebook_x86_64_gles"))
    532 		} else if parts["target_arch"] == "arm" {
    533 			pkgs = append(pkgs, b.MustGetCipdPackageFromAsset("armhf_sysroot"))
    534 			pkgs = append(pkgs, b.MustGetCipdPackageFromAsset("chromebook_arm_gles"))
    535 		}
    536 	} else if strings.Contains(name, "Debian") {
    537 		if strings.Contains(name, "Clang") {
    538 			pkgs = append(pkgs, b.MustGetCipdPackageFromAsset("clang_linux"))
    539 		}
    540 		if strings.Contains(name, "Vulkan") {
    541 			pkgs = append(pkgs, b.MustGetCipdPackageFromAsset("linux_vulkan_sdk"))
    542 		}
    543 		if strings.Contains(name, "EMCC") {
    544 			pkgs = append(pkgs, b.MustGetCipdPackageFromAsset("emscripten_sdk"))
    545 		}
    546 	} else if strings.Contains(name, "Win") {
    547 		deps = append(deps, isolateCIPDAsset(b, ISOLATE_WIN_TOOLCHAIN_NAME))
    548 		if strings.Contains(name, "Clang") {
    549 			pkgs = append(pkgs, b.MustGetCipdPackageFromAsset("clang_win"))
    550 		}
    551 		if strings.Contains(name, "Vulkan") {
    552 			deps = append(deps, isolateCIPDAsset(b, ISOLATE_WIN_VULKAN_SDK_NAME))
    553 		}
    554 	}
    555 
    556 	dimensions := swarmDimensions(parts)
    557 
    558 	// Add the task.
    559 	b.MustAddTask(name, &specs.TaskSpec{
    560 		CipdPackages: pkgs,
    561 		Dimensions:   dimensions,
    562 		Dependencies: deps,
    563 		ExtraArgs: []string{
    564 			"--workdir", "../../..", "compile",
    565 			fmt.Sprintf("repository=%s", specs.PLACEHOLDER_REPO),
    566 			fmt.Sprintf("buildername=%s", name),
    567 			fmt.Sprintf("swarm_out_dir=%s", specs.PLACEHOLDER_ISOLATED_OUTDIR),
    568 			fmt.Sprintf("revision=%s", specs.PLACEHOLDER_REVISION),
    569 			fmt.Sprintf("patch_repo=%s", specs.PLACEHOLDER_PATCH_REPO),
    570 			fmt.Sprintf("patch_storage=%s", specs.PLACEHOLDER_PATCH_STORAGE),
    571 			fmt.Sprintf("patch_issue=%s", specs.PLACEHOLDER_ISSUE),
    572 			fmt.Sprintf("patch_set=%s", specs.PLACEHOLDER_PATCHSET),
    573 		},
    574 		Isolate:  relpath("compile_skia.isolate"),
    575 		Priority: 0.8,
    576 	})
    577 	// All compile tasks are runnable as their own Job. Assert that the Job
    578 	// is listed in JOBS.
    579 	if !util.In(name, JOBS) {
    580 		glog.Fatalf("Job %q is missing from the JOBS list!", name)
    581 	}
    582 
    583 	// Upload the skiaserve binary only for Linux Android compile bots.
    584 	// See skbug.com/7399 for context.
    585 	if parts["configuration"] == "Release" &&
    586 		parts["extra_config"] == "Android" &&
    587 		!strings.Contains(parts["os"], "Win") &&
    588 		!strings.Contains(parts["os"], "Mac") {
    589 		uploadName := fmt.Sprintf("%s%s%s", PREFIX_UPLOAD, jobNameSchema.Sep, name)
    590 		b.MustAddTask(uploadName, &specs.TaskSpec{
    591 			Dependencies: []string{name},
    592 			Dimensions:   linuxGceDimensions(),
    593 			ExtraArgs: []string{
    594 				"--workdir", "../../..", "upload_skiaserve",
    595 				fmt.Sprintf("repository=%s", specs.PLACEHOLDER_REPO),
    596 				fmt.Sprintf("buildername=%s", name),
    597 				fmt.Sprintf("swarm_out_dir=%s", specs.PLACEHOLDER_ISOLATED_OUTDIR),
    598 				fmt.Sprintf("revision=%s", specs.PLACEHOLDER_REVISION),
    599 				fmt.Sprintf("patch_repo=%s", specs.PLACEHOLDER_PATCH_REPO),
    600 				fmt.Sprintf("patch_storage=%s", specs.PLACEHOLDER_PATCH_STORAGE),
    601 				fmt.Sprintf("patch_issue=%s", specs.PLACEHOLDER_ISSUE),
    602 				fmt.Sprintf("patch_set=%s", specs.PLACEHOLDER_PATCHSET),
    603 			},
    604 			// We're using the same isolate as upload_dm_results
    605 			Isolate:  relpath("upload_dm_results.isolate"),
    606 			Priority: 0.8,
    607 		})
    608 		return uploadName
    609 	}
    610 
    611 	return name
    612 }
    613 
    614 // recreateSKPs generates a RecreateSKPs task. Returns the name of the last
    615 // task in the generated chain of tasks, which the Job should add as a
    616 // dependency.
    617 func recreateSKPs(b *specs.TasksCfgBuilder, name string) string {
    618 	b.MustAddTask(name, &specs.TaskSpec{
    619 		CipdPackages:     []*specs.CipdPackage{b.MustGetCipdPackageFromAsset("go")},
    620 		Dimensions:       linuxGceDimensions(),
    621 		ExecutionTimeout: 4 * time.Hour,
    622 		ExtraArgs: []string{
    623 			"--workdir", "../../..", "recreate_skps",
    624 			fmt.Sprintf("repository=%s", specs.PLACEHOLDER_REPO),
    625 			fmt.Sprintf("buildername=%s", name),
    626 			fmt.Sprintf("swarm_out_dir=%s", specs.PLACEHOLDER_ISOLATED_OUTDIR),
    627 			fmt.Sprintf("revision=%s", specs.PLACEHOLDER_REVISION),
    628 			fmt.Sprintf("patch_repo=%s", specs.PLACEHOLDER_PATCH_REPO),
    629 			fmt.Sprintf("patch_storage=%s", specs.PLACEHOLDER_PATCH_STORAGE),
    630 			fmt.Sprintf("patch_issue=%s", specs.PLACEHOLDER_ISSUE),
    631 			fmt.Sprintf("patch_set=%s", specs.PLACEHOLDER_PATCHSET),
    632 		},
    633 		IoTimeout: 40 * time.Minute,
    634 		Isolate:   relpath("compile_skia.isolate"),
    635 		Priority:  0.8,
    636 	})
    637 	return name
    638 }
    639 
    640 // updateMetaConfig generates a UpdateMetaConfig task. Returns the name of the
    641 // last task in the generated chain of tasks, which the Job should add as a
    642 // dependency.
    643 func updateMetaConfig(b *specs.TasksCfgBuilder, name string) string {
    644 	b.MustAddTask(name, &specs.TaskSpec{
    645 		CipdPackages: []*specs.CipdPackage{},
    646 		Dimensions:   linuxGceDimensions(),
    647 		ExtraArgs: []string{
    648 			"--workdir", "../../..", "update_meta_config",
    649 			fmt.Sprintf("repository=%s", specs.PLACEHOLDER_REPO),
    650 			fmt.Sprintf("buildername=%s", name),
    651 			fmt.Sprintf("swarm_out_dir=%s", specs.PLACEHOLDER_ISOLATED_OUTDIR),
    652 			fmt.Sprintf("revision=%s", specs.PLACEHOLDER_REVISION),
    653 			fmt.Sprintf("patch_repo=%s", specs.PLACEHOLDER_PATCH_REPO),
    654 			fmt.Sprintf("patch_storage=%s", specs.PLACEHOLDER_PATCH_STORAGE),
    655 			fmt.Sprintf("patch_issue=%s", specs.PLACEHOLDER_ISSUE),
    656 			fmt.Sprintf("patch_set=%s", specs.PLACEHOLDER_PATCHSET),
    657 		},
    658 		Isolate:  relpath("meta_config.isolate"),
    659 		Priority: 0.8,
    660 	})
    661 	return name
    662 }
    663 
    664 // ctSKPs generates a CT SKPs task. Returns the name of the last task in the
    665 // generated chain of tasks, which the Job should add as a dependency.
    666 func ctSKPs(b *specs.TasksCfgBuilder, name string) string {
    667 	b.MustAddTask(name, &specs.TaskSpec{
    668 		CipdPackages: []*specs.CipdPackage{},
    669 		Dimensions: []string{
    670 			"pool:SkiaCT",
    671 			fmt.Sprintf("os:%s", DEFAULT_OS_LINUX_GCE),
    672 		},
    673 		ExecutionTimeout: 24 * time.Hour,
    674 		ExtraArgs: []string{
    675 			"--workdir", "../../..", "ct_skps",
    676 			fmt.Sprintf("repository=%s", specs.PLACEHOLDER_REPO),
    677 			fmt.Sprintf("buildername=%s", name),
    678 			fmt.Sprintf("swarm_out_dir=%s", specs.PLACEHOLDER_ISOLATED_OUTDIR),
    679 			fmt.Sprintf("revision=%s", specs.PLACEHOLDER_REVISION),
    680 			fmt.Sprintf("patch_repo=%s", specs.PLACEHOLDER_PATCH_REPO),
    681 			fmt.Sprintf("patch_storage=%s", specs.PLACEHOLDER_PATCH_STORAGE),
    682 			fmt.Sprintf("patch_issue=%s", specs.PLACEHOLDER_ISSUE),
    683 			fmt.Sprintf("patch_set=%s", specs.PLACEHOLDER_PATCHSET),
    684 		},
    685 		IoTimeout: time.Hour,
    686 		Isolate:   relpath("ct_skps_skia.isolate"),
    687 		Priority:  0.8,
    688 	})
    689 	return name
    690 }
    691 
    692 // checkGeneratedFiles verifies that no generated SKSL files have been edited
    693 // by hand.
    694 func checkGeneratedFiles(b *specs.TasksCfgBuilder, name string) string {
    695 	b.MustAddTask(name, &specs.TaskSpec{
    696 		CipdPackages: []*specs.CipdPackage{},
    697 		Dimensions:   linuxGceDimensions(),
    698 		ExtraArgs: []string{
    699 			"--workdir", "../../..", "check_generated_files",
    700 			fmt.Sprintf("repository=%s", specs.PLACEHOLDER_REPO),
    701 			fmt.Sprintf("buildername=%s", name),
    702 			fmt.Sprintf("swarm_out_dir=%s", specs.PLACEHOLDER_ISOLATED_OUTDIR),
    703 			fmt.Sprintf("revision=%s", specs.PLACEHOLDER_REVISION),
    704 			fmt.Sprintf("patch_repo=%s", specs.PLACEHOLDER_PATCH_REPO),
    705 			fmt.Sprintf("patch_storage=%s", specs.PLACEHOLDER_PATCH_STORAGE),
    706 			fmt.Sprintf("patch_issue=%s", specs.PLACEHOLDER_ISSUE),
    707 			fmt.Sprintf("patch_set=%s", specs.PLACEHOLDER_PATCHSET),
    708 		},
    709 		Isolate:  relpath("compile_skia.isolate"),
    710 		Priority: 0.8,
    711 	})
    712 	return name
    713 }
    714 
    715 // housekeeper generates a Housekeeper task. Returns the name of the last task
    716 // in the generated chain of tasks, which the Job should add as a dependency.
    717 func housekeeper(b *specs.TasksCfgBuilder, name, compileTaskName string) string {
    718 	b.MustAddTask(name, &specs.TaskSpec{
    719 		CipdPackages: []*specs.CipdPackage{b.MustGetCipdPackageFromAsset("go")},
    720 		Dependencies: []string{compileTaskName},
    721 		Dimensions:   linuxGceDimensions(),
    722 		ExtraArgs: []string{
    723 			"--workdir", "../../..", "housekeeper",
    724 			fmt.Sprintf("repository=%s", specs.PLACEHOLDER_REPO),
    725 			fmt.Sprintf("buildername=%s", name),
    726 			fmt.Sprintf("swarm_out_dir=%s", specs.PLACEHOLDER_ISOLATED_OUTDIR),
    727 			fmt.Sprintf("revision=%s", specs.PLACEHOLDER_REVISION),
    728 			fmt.Sprintf("patch_repo=%s", specs.PLACEHOLDER_PATCH_REPO),
    729 			fmt.Sprintf("patch_storage=%s", specs.PLACEHOLDER_PATCH_STORAGE),
    730 			fmt.Sprintf("patch_issue=%s", specs.PLACEHOLDER_ISSUE),
    731 			fmt.Sprintf("patch_set=%s", specs.PLACEHOLDER_PATCHSET),
    732 		},
    733 		Isolate:  relpath("housekeeper_skia.isolate"),
    734 		Priority: 0.8,
    735 	})
    736 	return name
    737 }
    738 
    739 // bookmaker generates a Bookmaker task. Returns the name of the last task
    740 // in the generated chain of tasks, which the Job should add as a dependency.
    741 func bookmaker(b *specs.TasksCfgBuilder, name, compileTaskName string) string {
    742 	b.MustAddTask(name, &specs.TaskSpec{
    743 		CipdPackages: []*specs.CipdPackage{b.MustGetCipdPackageFromAsset("go")},
    744 		Dependencies: []string{compileTaskName},
    745 		Dimensions:   linuxGceDimensions(),
    746 		ExtraArgs: []string{
    747 			"--workdir", "../../..", "bookmaker",
    748 			fmt.Sprintf("repository=%s", specs.PLACEHOLDER_REPO),
    749 			fmt.Sprintf("buildername=%s", name),
    750 			fmt.Sprintf("swarm_out_dir=%s", specs.PLACEHOLDER_ISOLATED_OUTDIR),
    751 			fmt.Sprintf("revision=%s", specs.PLACEHOLDER_REVISION),
    752 			fmt.Sprintf("patch_repo=%s", specs.PLACEHOLDER_PATCH_REPO),
    753 			fmt.Sprintf("patch_storage=%s", specs.PLACEHOLDER_PATCH_STORAGE),
    754 			fmt.Sprintf("patch_issue=%s", specs.PLACEHOLDER_ISSUE),
    755 			fmt.Sprintf("patch_set=%s", specs.PLACEHOLDER_PATCHSET),
    756 		},
    757 		Isolate:          relpath("compile_skia.isolate"),
    758 		Priority:         0.8,
    759 		ExecutionTimeout: 2 * time.Hour,
    760 		IoTimeout:        2 * time.Hour,
    761 	})
    762 	return name
    763 }
    764 
    765 // androidFrameworkCompile generates an Android Framework Compile task. Returns
    766 // the name of the last task in the generated chain of tasks, which the Job
    767 // should add as a dependency.
    768 func androidFrameworkCompile(b *specs.TasksCfgBuilder, name string) string {
    769 	b.MustAddTask(name, &specs.TaskSpec{
    770 		Dimensions: linuxGceDimensions(),
    771 		ExtraArgs: []string{
    772 			"--workdir", "../../..", "android_compile",
    773 			fmt.Sprintf("repository=%s", specs.PLACEHOLDER_REPO),
    774 			fmt.Sprintf("buildername=%s", name),
    775 			fmt.Sprintf("swarm_out_dir=%s", specs.PLACEHOLDER_ISOLATED_OUTDIR),
    776 			fmt.Sprintf("revision=%s", specs.PLACEHOLDER_REVISION),
    777 			fmt.Sprintf("patch_repo=%s", specs.PLACEHOLDER_PATCH_REPO),
    778 			fmt.Sprintf("patch_storage=%s", specs.PLACEHOLDER_PATCH_STORAGE),
    779 			fmt.Sprintf("patch_issue=%s", specs.PLACEHOLDER_ISSUE),
    780 			fmt.Sprintf("patch_set=%s", specs.PLACEHOLDER_PATCHSET),
    781 		},
    782 		Isolate:  relpath("compile_skia.isolate"),
    783 		Priority: 0.8,
    784 	})
    785 	return name
    786 }
    787 
    788 // infra generates an infra_tests task. Returns the name of the last task in the
    789 // generated chain of tasks, which the Job should add as a dependency.
    790 func infra(b *specs.TasksCfgBuilder, name string) string {
    791 	b.MustAddTask(name, &specs.TaskSpec{
    792 		CipdPackages: []*specs.CipdPackage{b.MustGetCipdPackageFromAsset("go")},
    793 		Dimensions:   linuxGceDimensions(),
    794 		ExtraArgs: []string{
    795 			"--workdir", "../../..", "infra",
    796 			fmt.Sprintf("repository=%s", specs.PLACEHOLDER_REPO),
    797 			fmt.Sprintf("buildername=%s", name),
    798 			fmt.Sprintf("swarm_out_dir=%s", specs.PLACEHOLDER_ISOLATED_OUTDIR),
    799 			fmt.Sprintf("revision=%s", specs.PLACEHOLDER_REVISION),
    800 			fmt.Sprintf("patch_repo=%s", specs.PLACEHOLDER_PATCH_REPO),
    801 			fmt.Sprintf("patch_storage=%s", specs.PLACEHOLDER_PATCH_STORAGE),
    802 			fmt.Sprintf("patch_issue=%s", specs.PLACEHOLDER_ISSUE),
    803 			fmt.Sprintf("patch_set=%s", specs.PLACEHOLDER_PATCHSET),
    804 		},
    805 		Isolate:  relpath("infra_skia.isolate"),
    806 		Priority: 0.8,
    807 	})
    808 	return name
    809 }
    810 
    811 func getParentRevisionName(compileTaskName string, parts map[string]string) string {
    812 	if parts["extra_config"] == "" {
    813 		return compileTaskName + "-ParentRevision"
    814 	} else {
    815 		return compileTaskName + "_ParentRevision"
    816 	}
    817 }
    818 
    819 // calmbench generates a calmbench task. Returns the name of the last task in the
    820 // generated chain of tasks, which the Job should add as a dependency.
    821 func calmbench(b *specs.TasksCfgBuilder, name string, parts map[string]string, compileTaskName string, compileParentName string) string {
    822 	s := &specs.TaskSpec{
    823 		Dependencies: []string{compileTaskName, compileParentName},
    824 		CipdPackages: []*specs.CipdPackage{b.MustGetCipdPackageFromAsset("clang_linux")},
    825 		Dimensions:   swarmDimensions(parts),
    826 		ExtraArgs: []string{
    827 			"--workdir", "../../..", "calmbench",
    828 			fmt.Sprintf("repository=%s", specs.PLACEHOLDER_REPO),
    829 			fmt.Sprintf("buildername=%s", name),
    830 			fmt.Sprintf("swarm_out_dir=%s", specs.PLACEHOLDER_ISOLATED_OUTDIR),
    831 			fmt.Sprintf("revision=%s", specs.PLACEHOLDER_REVISION),
    832 			fmt.Sprintf("patch_repo=%s", specs.PLACEHOLDER_PATCH_REPO),
    833 			fmt.Sprintf("patch_storage=%s", specs.PLACEHOLDER_PATCH_STORAGE),
    834 			fmt.Sprintf("patch_issue=%s", specs.PLACEHOLDER_ISSUE),
    835 			fmt.Sprintf("patch_set=%s", specs.PLACEHOLDER_PATCHSET),
    836 		},
    837 		Isolate:  relpath("calmbench.isolate"),
    838 		Priority: 0.8,
    839 	}
    840 
    841 	s.Dependencies = append(s.Dependencies, ISOLATE_SKP_NAME, ISOLATE_SVG_NAME)
    842 
    843 	b.MustAddTask(name, s)
    844 
    845 	// Upload results if necessary.
    846 	if strings.Contains(name, "Release") && doUpload(name) {
    847 		uploadName := fmt.Sprintf("%s%s%s", PREFIX_UPLOAD, jobNameSchema.Sep, name)
    848 		b.MustAddTask(uploadName, &specs.TaskSpec{
    849 			Dependencies: []string{name},
    850 			Dimensions:   linuxGceDimensions(),
    851 			ExtraArgs: []string{
    852 				"--workdir", "../../..", "upload_calmbench_results",
    853 				fmt.Sprintf("repository=%s", specs.PLACEHOLDER_REPO),
    854 				fmt.Sprintf("buildername=%s", name),
    855 				fmt.Sprintf("swarm_out_dir=%s", specs.PLACEHOLDER_ISOLATED_OUTDIR),
    856 				fmt.Sprintf("revision=%s", specs.PLACEHOLDER_REVISION),
    857 				fmt.Sprintf("patch_repo=%s", specs.PLACEHOLDER_PATCH_REPO),
    858 				fmt.Sprintf("patch_storage=%s", specs.PLACEHOLDER_PATCH_STORAGE),
    859 				fmt.Sprintf("patch_issue=%s", specs.PLACEHOLDER_ISSUE),
    860 				fmt.Sprintf("patch_set=%s", specs.PLACEHOLDER_PATCHSET),
    861 				fmt.Sprintf("gs_bucket=%s", CONFIG.GsBucketCalm),
    862 			},
    863 			// We're using the same isolate as upload_nano_results
    864 			Isolate:  relpath("upload_nano_results.isolate"),
    865 			Priority: 0.8,
    866 		})
    867 		return uploadName
    868 	}
    869 
    870 	return name
    871 }
    872 
    873 // doUpload indicates whether the given Job should upload its results.
    874 func doUpload(name string) bool {
    875 	for _, s := range CONFIG.NoUpload {
    876 		m, err := regexp.MatchString(s, name)
    877 		if err != nil {
    878 			glog.Fatal(err)
    879 		}
    880 		if m {
    881 			return false
    882 		}
    883 	}
    884 	return true
    885 }
    886 
    887 // test generates a Test task. Returns the name of the last task in the
    888 // generated chain of tasks, which the Job should add as a dependency.
    889 func test(b *specs.TasksCfgBuilder, name string, parts map[string]string, compileTaskName string, pkgs []*specs.CipdPackage) string {
    890 	deps := []string{compileTaskName}
    891 	if strings.Contains(name, "Android_ASAN") {
    892 		deps = append(deps, isolateCIPDAsset(b, ISOLATE_NDK_LINUX_NAME))
    893 	}
    894 
    895 	s := &specs.TaskSpec{
    896 		CipdPackages:     pkgs,
    897 		Dependencies:     deps,
    898 		Dimensions:       swarmDimensions(parts),
    899 		ExecutionTimeout: 4 * time.Hour,
    900 		Expiration:       20 * time.Hour,
    901 		ExtraArgs: []string{
    902 			"--workdir", "../../..", "test",
    903 			fmt.Sprintf("repository=%s", specs.PLACEHOLDER_REPO),
    904 			fmt.Sprintf("buildbucket_build_id=%s", specs.PLACEHOLDER_BUILDBUCKET_BUILD_ID),
    905 			fmt.Sprintf("buildername=%s", name),
    906 			fmt.Sprintf("swarm_out_dir=%s", specs.PLACEHOLDER_ISOLATED_OUTDIR),
    907 			fmt.Sprintf("revision=%s", specs.PLACEHOLDER_REVISION),
    908 			fmt.Sprintf("patch_repo=%s", specs.PLACEHOLDER_PATCH_REPO),
    909 			fmt.Sprintf("patch_storage=%s", specs.PLACEHOLDER_PATCH_STORAGE),
    910 			fmt.Sprintf("patch_issue=%s", specs.PLACEHOLDER_ISSUE),
    911 			fmt.Sprintf("patch_set=%s", specs.PLACEHOLDER_PATCHSET),
    912 		},
    913 		IoTimeout:   40 * time.Minute,
    914 		Isolate:     relpath("test_skia.isolate"),
    915 		MaxAttempts: 1,
    916 		Priority:    0.8,
    917 	}
    918 	if useBundledRecipes(parts) {
    919 		s.Dependencies = append(s.Dependencies, BUNDLE_RECIPES_NAME)
    920 		if strings.Contains(parts["os"], "Win") {
    921 			s.Isolate = relpath("test_skia_bundled_win.isolate")
    922 		} else {
    923 			s.Isolate = relpath("test_skia_bundled_unix.isolate")
    924 		}
    925 	}
    926 	if deps := getIsolatedCIPDDeps(parts); len(deps) > 0 {
    927 		s.Dependencies = append(s.Dependencies, deps...)
    928 	}
    929 	if strings.Contains(parts["extra_config"], "Valgrind") {
    930 		s.ExecutionTimeout = 9 * time.Hour
    931 		s.Expiration = 48 * time.Hour
    932 		s.IoTimeout = time.Hour
    933 		s.CipdPackages = append(s.CipdPackages, b.MustGetCipdPackageFromAsset("valgrind"))
    934 		s.Dimensions = append(s.Dimensions, "valgrind:1")
    935 	} else if strings.Contains(parts["extra_config"], "MSAN") {
    936 		s.ExecutionTimeout = 9 * time.Hour
    937 	} else if parts["arch"] == "x86" && parts["configuration"] == "Debug" {
    938 		// skia:6737
    939 		s.ExecutionTimeout = 6 * time.Hour
    940 	}
    941 	iid := internalHardwareLabel(parts)
    942 	if iid != nil {
    943 		s.ExtraArgs = append(s.ExtraArgs, fmt.Sprintf("internal_hardware_label=%d", *iid))
    944 	}
    945 	b.MustAddTask(name, s)
    946 
    947 	// Upload results if necessary. TODO(kjlubick): If we do coverage analysis at the same
    948 	// time as normal tests (which would be nice), cfg.json needs to have Coverage removed.
    949 	if doUpload(name) {
    950 		uploadName := fmt.Sprintf("%s%s%s", PREFIX_UPLOAD, jobNameSchema.Sep, name)
    951 		b.MustAddTask(uploadName, &specs.TaskSpec{
    952 			Dependencies: []string{name},
    953 			Dimensions:   linuxGceDimensions(),
    954 			ExtraArgs: []string{
    955 				"--workdir", "../../..", "upload_dm_results",
    956 				fmt.Sprintf("repository=%s", specs.PLACEHOLDER_REPO),
    957 				fmt.Sprintf("buildername=%s", name),
    958 				fmt.Sprintf("swarm_out_dir=%s", specs.PLACEHOLDER_ISOLATED_OUTDIR),
    959 				fmt.Sprintf("revision=%s", specs.PLACEHOLDER_REVISION),
    960 				fmt.Sprintf("patch_repo=%s", specs.PLACEHOLDER_PATCH_REPO),
    961 				fmt.Sprintf("patch_storage=%s", specs.PLACEHOLDER_PATCH_STORAGE),
    962 				fmt.Sprintf("patch_issue=%s", specs.PLACEHOLDER_ISSUE),
    963 				fmt.Sprintf("patch_set=%s", specs.PLACEHOLDER_PATCHSET),
    964 				fmt.Sprintf("gs_bucket=%s", CONFIG.GsBucketGm),
    965 			},
    966 			Isolate:  relpath("upload_dm_results.isolate"),
    967 			Priority: 0.8,
    968 		})
    969 		return uploadName
    970 	}
    971 
    972 	return name
    973 }
    974 
    975 func coverage(b *specs.TasksCfgBuilder, name string, parts map[string]string, compileTaskName string, pkgs []*specs.CipdPackage) string {
    976 	shards := 1
    977 	deps := []string{}
    978 
    979 	tf := parts["test_filter"]
    980 	if strings.Contains(tf, "Shard") {
    981 		// Expected Shard_NN
    982 		shardstr := strings.Split(tf, "_")[1]
    983 		var err error
    984 		shards, err = strconv.Atoi(shardstr)
    985 		if err != nil {
    986 			glog.Fatalf("Expected int for number of shards %q in %s: %s", shardstr, name, err)
    987 		}
    988 	}
    989 	for i := 0; i < shards; i++ {
    990 		n := strings.Replace(name, tf, fmt.Sprintf("shard_%02d_%02d", i, shards), 1)
    991 		s := &specs.TaskSpec{
    992 			CipdPackages:     pkgs,
    993 			Dependencies:     []string{compileTaskName},
    994 			Dimensions:       swarmDimensions(parts),
    995 			ExecutionTimeout: 4 * time.Hour,
    996 			Expiration:       20 * time.Hour,
    997 			ExtraArgs: []string{
    998 				"--workdir", "../../..", "test",
    999 				fmt.Sprintf("repository=%s", specs.PLACEHOLDER_REPO),
   1000 				fmt.Sprintf("buildername=%s", n),
   1001 				fmt.Sprintf("swarm_out_dir=%s", specs.PLACEHOLDER_ISOLATED_OUTDIR),
   1002 				fmt.Sprintf("revision=%s", specs.PLACEHOLDER_REVISION),
   1003 				fmt.Sprintf("patch_repo=%s", specs.PLACEHOLDER_PATCH_REPO),
   1004 				fmt.Sprintf("patch_storage=%s", specs.PLACEHOLDER_PATCH_STORAGE),
   1005 				fmt.Sprintf("patch_issue=%s", specs.PLACEHOLDER_ISSUE),
   1006 				fmt.Sprintf("patch_set=%s", specs.PLACEHOLDER_PATCHSET),
   1007 			},
   1008 			IoTimeout:   40 * time.Minute,
   1009 			Isolate:     relpath("test_skia.isolate"),
   1010 			MaxAttempts: 1,
   1011 			Priority:    0.8,
   1012 		}
   1013 		if useBundledRecipes(parts) {
   1014 			s.Dependencies = append(s.Dependencies, BUNDLE_RECIPES_NAME)
   1015 			if strings.Contains(parts["os"], "Win") {
   1016 				s.Isolate = relpath("test_skia_bundled_win.isolate")
   1017 			} else {
   1018 				s.Isolate = relpath("test_skia_bundled_unix.isolate")
   1019 			}
   1020 		}
   1021 		if deps := getIsolatedCIPDDeps(parts); len(deps) > 0 {
   1022 			s.Dependencies = append(s.Dependencies, deps...)
   1023 		}
   1024 		b.MustAddTask(n, s)
   1025 		deps = append(deps, n)
   1026 	}
   1027 
   1028 	uploadName := fmt.Sprintf("%s%s%s", "Upload", jobNameSchema.Sep, name)
   1029 	// We need clang_linux to get access to the llvm-profdata and llvm-cov binaries
   1030 	// which are used to deal with the raw coverage data output by the Test step.
   1031 	pkgs = append([]*specs.CipdPackage{}, b.MustGetCipdPackageFromAsset("clang_linux"))
   1032 	deps = append(deps, compileTaskName)
   1033 
   1034 	b.MustAddTask(uploadName, &specs.TaskSpec{
   1035 		// A dependency on compileTaskName makes the TaskScheduler link the
   1036 		// isolated output of the compile step to the input of the upload step,
   1037 		// which gives us access to the instrumented binary. The binary is
   1038 		// needed to figure out symbol names and line numbers.
   1039 		Dependencies: deps,
   1040 		Dimensions:   linuxGceDimensions(),
   1041 		CipdPackages: pkgs,
   1042 		ExtraArgs: []string{
   1043 			"--workdir", "../../..", "upload_coverage_results",
   1044 			fmt.Sprintf("repository=%s", specs.PLACEHOLDER_REPO),
   1045 			fmt.Sprintf("buildername=%s", name),
   1046 			fmt.Sprintf("swarm_out_dir=%s", specs.PLACEHOLDER_ISOLATED_OUTDIR),
   1047 			fmt.Sprintf("revision=%s", specs.PLACEHOLDER_REVISION),
   1048 			fmt.Sprintf("patch_repo=%s", specs.PLACEHOLDER_PATCH_REPO),
   1049 			fmt.Sprintf("patch_storage=%s", specs.PLACEHOLDER_PATCH_STORAGE),
   1050 			fmt.Sprintf("patch_issue=%s", specs.PLACEHOLDER_ISSUE),
   1051 			fmt.Sprintf("patch_set=%s", specs.PLACEHOLDER_PATCHSET),
   1052 			fmt.Sprintf("gs_bucket=%s", CONFIG.GsBucketCoverage),
   1053 		},
   1054 		Isolate:  relpath("upload_coverage_results.isolate"),
   1055 		Priority: 0.8,
   1056 	})
   1057 	return uploadName
   1058 }
   1059 
   1060 // perf generates a Perf task. Returns the name of the last task in the
   1061 // generated chain of tasks, which the Job should add as a dependency.
   1062 func perf(b *specs.TasksCfgBuilder, name string, parts map[string]string, compileTaskName string, pkgs []*specs.CipdPackage) string {
   1063 	recipe := "perf"
   1064 	isolate := relpath("perf_skia.isolate")
   1065 	if strings.Contains(parts["extra_config"], "Skpbench") {
   1066 		recipe = "skpbench"
   1067 		isolate = relpath("skpbench_skia.isolate")
   1068 		if useBundledRecipes(parts) {
   1069 			if strings.Contains(parts["os"], "Win") {
   1070 				isolate = relpath("skpbench_skia_bundled_win.isolate")
   1071 			} else {
   1072 				isolate = relpath("skpbench_skia_bundled_unix.isolate")
   1073 			}
   1074 		}
   1075 	} else if useBundledRecipes(parts) {
   1076 		if strings.Contains(parts["os"], "Win") {
   1077 			isolate = relpath("perf_skia_bundled_win.isolate")
   1078 		} else {
   1079 			isolate = relpath("perf_skia_bundled_unix.isolate")
   1080 		}
   1081 	}
   1082 	s := &specs.TaskSpec{
   1083 		CipdPackages:     pkgs,
   1084 		Dependencies:     []string{compileTaskName},
   1085 		Dimensions:       swarmDimensions(parts),
   1086 		ExecutionTimeout: 4 * time.Hour,
   1087 		Expiration:       20 * time.Hour,
   1088 		ExtraArgs: []string{
   1089 			"--workdir", "../../..", recipe,
   1090 			fmt.Sprintf("repository=%s", specs.PLACEHOLDER_REPO),
   1091 			fmt.Sprintf("buildername=%s", name),
   1092 			fmt.Sprintf("swarm_out_dir=%s", specs.PLACEHOLDER_ISOLATED_OUTDIR),
   1093 			fmt.Sprintf("revision=%s", specs.PLACEHOLDER_REVISION),
   1094 			fmt.Sprintf("patch_repo=%s", specs.PLACEHOLDER_PATCH_REPO),
   1095 			fmt.Sprintf("patch_storage=%s", specs.PLACEHOLDER_PATCH_STORAGE),
   1096 			fmt.Sprintf("patch_issue=%s", specs.PLACEHOLDER_ISSUE),
   1097 			fmt.Sprintf("patch_set=%s", specs.PLACEHOLDER_PATCHSET),
   1098 		},
   1099 		IoTimeout:   40 * time.Minute,
   1100 		Isolate:     isolate,
   1101 		MaxAttempts: 1,
   1102 		Priority:    0.8,
   1103 	}
   1104 	if useBundledRecipes(parts) {
   1105 		s.Dependencies = append(s.Dependencies, BUNDLE_RECIPES_NAME)
   1106 	}
   1107 	if deps := getIsolatedCIPDDeps(parts); len(deps) > 0 {
   1108 		s.Dependencies = append(s.Dependencies, deps...)
   1109 	}
   1110 
   1111 	if strings.Contains(parts["extra_config"], "Valgrind") {
   1112 		s.ExecutionTimeout = 9 * time.Hour
   1113 		s.Expiration = 48 * time.Hour
   1114 		s.IoTimeout = time.Hour
   1115 		s.CipdPackages = append(s.CipdPackages, b.MustGetCipdPackageFromAsset("valgrind"))
   1116 		s.Dimensions = append(s.Dimensions, "valgrind:1")
   1117 	} else if strings.Contains(parts["extra_config"], "MSAN") {
   1118 		s.ExecutionTimeout = 9 * time.Hour
   1119 	} else if parts["arch"] == "x86" && parts["configuration"] == "Debug" {
   1120 		// skia:6737
   1121 		s.ExecutionTimeout = 6 * time.Hour
   1122 	}
   1123 	iid := internalHardwareLabel(parts)
   1124 	if iid != nil {
   1125 		s.ExtraArgs = append(s.ExtraArgs, fmt.Sprintf("internal_hardware_label=%d", *iid))
   1126 	}
   1127 	b.MustAddTask(name, s)
   1128 
   1129 	// Upload results if necessary.
   1130 	if strings.Contains(name, "Release") && doUpload(name) {
   1131 		uploadName := fmt.Sprintf("%s%s%s", PREFIX_UPLOAD, jobNameSchema.Sep, name)
   1132 		b.MustAddTask(uploadName, &specs.TaskSpec{
   1133 			Dependencies: []string{name},
   1134 			Dimensions:   linuxGceDimensions(),
   1135 			ExtraArgs: []string{
   1136 				"--workdir", "../../..", "upload_nano_results",
   1137 				fmt.Sprintf("repository=%s", specs.PLACEHOLDER_REPO),
   1138 				fmt.Sprintf("buildername=%s", name),
   1139 				fmt.Sprintf("swarm_out_dir=%s", specs.PLACEHOLDER_ISOLATED_OUTDIR),
   1140 				fmt.Sprintf("revision=%s", specs.PLACEHOLDER_REVISION),
   1141 				fmt.Sprintf("patch_repo=%s", specs.PLACEHOLDER_PATCH_REPO),
   1142 				fmt.Sprintf("patch_storage=%s", specs.PLACEHOLDER_PATCH_STORAGE),
   1143 				fmt.Sprintf("patch_issue=%s", specs.PLACEHOLDER_ISSUE),
   1144 				fmt.Sprintf("patch_set=%s", specs.PLACEHOLDER_PATCHSET),
   1145 				fmt.Sprintf("gs_bucket=%s", CONFIG.GsBucketNano),
   1146 			},
   1147 			Isolate:  relpath("upload_nano_results.isolate"),
   1148 			Priority: 0.8,
   1149 		})
   1150 		return uploadName
   1151 	}
   1152 	return name
   1153 }
   1154 
   1155 // process generates tasks and jobs for the given job name.
   1156 func process(b *specs.TasksCfgBuilder, name string) {
   1157 	deps := []string{}
   1158 
   1159 	// Bundle Recipes.
   1160 	if name == BUNDLE_RECIPES_NAME {
   1161 		deps = append(deps, bundleRecipes(b))
   1162 	}
   1163 
   1164 	// Isolate CIPD assets.
   1165 	if _, ok := ISOLATE_ASSET_MAPPING[name]; ok {
   1166 		deps = append(deps, isolateCIPDAsset(b, name))
   1167 	}
   1168 
   1169 	parts, err := jobNameSchema.ParseJobName(name)
   1170 	if err != nil {
   1171 		glog.Fatal(err)
   1172 	}
   1173 
   1174 	// RecreateSKPs.
   1175 	if strings.Contains(name, "RecreateSKPs") {
   1176 		deps = append(deps, recreateSKPs(b, name))
   1177 	}
   1178 
   1179 	// UpdateMetaConfig bot.
   1180 	if strings.Contains(name, "UpdateMetaConfig") {
   1181 		deps = append(deps, updateMetaConfig(b, name))
   1182 	}
   1183 
   1184 	// CT bots.
   1185 	if strings.Contains(name, "-CT_") {
   1186 		deps = append(deps, ctSKPs(b, name))
   1187 	}
   1188 
   1189 	// Infra tests.
   1190 	if name == "Housekeeper-PerCommit-InfraTests" {
   1191 		deps = append(deps, infra(b, name))
   1192 	}
   1193 
   1194 	// Compile bots.
   1195 	if parts["role"] == "Build" {
   1196 		if parts["extra_config"] == "Android_Framework" {
   1197 			// Android Framework compile tasks use a different recipe.
   1198 			deps = append(deps, androidFrameworkCompile(b, name))
   1199 		} else {
   1200 			deps = append(deps, compile(b, name, parts))
   1201 		}
   1202 	}
   1203 
   1204 	// Most remaining bots need a compile task.
   1205 	compileTaskName := deriveCompileTaskName(name, parts)
   1206 	compileTaskParts, err := jobNameSchema.ParseJobName(compileTaskName)
   1207 	if err != nil {
   1208 		glog.Fatal(err)
   1209 	}
   1210 	compileParentName := getParentRevisionName(compileTaskName, compileTaskParts)
   1211 	compileParentParts, err := jobNameSchema.ParseJobName(compileParentName)
   1212 	if err != nil {
   1213 		glog.Fatal(err)
   1214 	}
   1215 
   1216 	// These bots do not need a compile task.
   1217 	if parts["role"] != "Build" &&
   1218 		name != "Housekeeper-PerCommit-BundleRecipes" &&
   1219 		name != "Housekeeper-PerCommit-InfraTests" &&
   1220 		name != "Housekeeper-PerCommit-CheckGeneratedFiles" &&
   1221 		!strings.Contains(name, "Android_Framework") &&
   1222 		!strings.Contains(name, "RecreateSKPs") &&
   1223 		!strings.Contains(name, "UpdateMetaConfig") &&
   1224 		!strings.Contains(name, "-CT_") &&
   1225 		!strings.Contains(name, "Housekeeper-PerCommit-Isolate") {
   1226 		compile(b, compileTaskName, compileTaskParts)
   1227 		if parts["role"] == "Calmbench" {
   1228 			compile(b, compileParentName, compileParentParts)
   1229 		}
   1230 	}
   1231 
   1232 	// Housekeepers.
   1233 	if name == "Housekeeper-PerCommit" {
   1234 		deps = append(deps, housekeeper(b, name, compileTaskName))
   1235 	}
   1236 	if name == "Housekeeper-PerCommit-CheckGeneratedFiles" {
   1237 		deps = append(deps, checkGeneratedFiles(b, name))
   1238 	}
   1239 	if strings.Contains(name, "Bookmaker") {
   1240 		deps = append(deps, bookmaker(b, name, compileTaskName))
   1241 	}
   1242 
   1243 	// Common assets needed by the remaining bots.
   1244 
   1245 	pkgs := []*specs.CipdPackage{}
   1246 
   1247 	if deps := getIsolatedCIPDDeps(parts); len(deps) == 0 {
   1248 		pkgs = []*specs.CipdPackage{
   1249 			b.MustGetCipdPackageFromAsset("skimage"),
   1250 			b.MustGetCipdPackageFromAsset("skp"),
   1251 			b.MustGetCipdPackageFromAsset("svg"),
   1252 		}
   1253 	}
   1254 
   1255 	if strings.Contains(name, "Ubuntu") || strings.Contains(name, "Debian") {
   1256 		if strings.Contains(name, "SAN") {
   1257 			pkgs = append(pkgs, b.MustGetCipdPackageFromAsset("clang_linux"))
   1258 		}
   1259 		if strings.Contains(name, "Vulkan") {
   1260 			pkgs = append(pkgs, b.MustGetCipdPackageFromAsset("linux_vulkan_sdk"))
   1261 		}
   1262 		if strings.Contains(name, "Intel") && strings.Contains(name, "GPU") {
   1263 			if strings.Contains(name, "Release") {
   1264 				pkgs = append(pkgs, b.MustGetCipdPackageFromAsset("linux_vulkan_intel_driver_release"))
   1265 			} else {
   1266 				pkgs = append(pkgs, b.MustGetCipdPackageFromAsset("linux_vulkan_intel_driver_debug"))
   1267 			}
   1268 		}
   1269 	}
   1270 
   1271 	// Test bots.
   1272 
   1273 	if parts["role"] == "Test" {
   1274 		if strings.Contains(parts["extra_config"], "Coverage") {
   1275 			deps = append(deps, coverage(b, name, parts, compileTaskName, pkgs))
   1276 		} else if !strings.Contains(name, "-CT_") {
   1277 			deps = append(deps, test(b, name, parts, compileTaskName, pkgs))
   1278 		}
   1279 
   1280 	}
   1281 
   1282 	// Perf bots.
   1283 	if parts["role"] == "Perf" && !strings.Contains(name, "-CT_") {
   1284 		deps = append(deps, perf(b, name, parts, compileTaskName, pkgs))
   1285 	}
   1286 
   1287 	// Calmbench bots.
   1288 	if parts["role"] == "Calmbench" {
   1289 		deps = append(deps, calmbench(b, name, parts, compileTaskName, compileParentName))
   1290 	}
   1291 
   1292 	// Add the Job spec.
   1293 	j := &specs.JobSpec{
   1294 		Priority:  0.8,
   1295 		TaskSpecs: deps,
   1296 		Trigger:   specs.TRIGGER_ANY_BRANCH,
   1297 	}
   1298 	if strings.Contains(name, "-Nightly-") {
   1299 		j.Trigger = specs.TRIGGER_NIGHTLY
   1300 	} else if strings.Contains(name, "-Weekly-") || strings.Contains(name, "CT_DM_1m_SKPs") {
   1301 		j.Trigger = specs.TRIGGER_WEEKLY
   1302 	} else if strings.Contains(name, "Flutter") || strings.Contains(name, "PDFium") || strings.Contains(name, "CommandBuffer") {
   1303 		j.Trigger = specs.TRIGGER_MASTER_ONLY
   1304 	}
   1305 	b.MustAddJob(name, j)
   1306 }
   1307 
   1308 func loadJson(flag *string, defaultFlag string, val interface{}) {
   1309 	if *flag == "" {
   1310 		*flag = defaultFlag
   1311 	}
   1312 	b, err := ioutil.ReadFile(*flag)
   1313 	if err != nil {
   1314 		glog.Fatal(err)
   1315 	}
   1316 	if err := json.Unmarshal(b, val); err != nil {
   1317 		glog.Fatal(err)
   1318 	}
   1319 }
   1320 
   1321 // Regenerate the tasks.json file.
   1322 func main() {
   1323 	b := specs.MustNewTasksCfgBuilder()
   1324 	b.SetAssetsDir(*assetsDir)
   1325 	infraBots := path.Join(b.CheckoutRoot(), "infra", "bots")
   1326 
   1327 	// Load the jobs from a JSON file.
   1328 	loadJson(jobsFile, path.Join(infraBots, "jobs.json"), &JOBS)
   1329 
   1330 	// Load general config information from a JSON file.
   1331 	loadJson(cfgFile, path.Join(infraBots, "cfg.json"), &CONFIG)
   1332 
   1333 	// Create the JobNameSchema.
   1334 	if *builderNameSchemaFile == "" {
   1335 		*builderNameSchemaFile = path.Join(b.CheckoutRoot(), "infra", "bots", "recipe_modules", "builder_name_schema", "builder_name_schema.json")
   1336 	}
   1337 	schema, err := NewJobNameSchema(*builderNameSchemaFile)
   1338 	if err != nil {
   1339 		glog.Fatal(err)
   1340 	}
   1341 	jobNameSchema = schema
   1342 
   1343 	// Create Tasks and Jobs.
   1344 	for _, name := range JOBS {
   1345 		process(b, name)
   1346 	}
   1347 
   1348 	b.MustFinish()
   1349 }
   1350 
   1351 // TODO(borenet): The below really belongs in its own file, probably next to the
   1352 // builder_name_schema.json file.
   1353 
   1354 // JobNameSchema is a struct used for (de)constructing Job names in a
   1355 // predictable format.
   1356 type JobNameSchema struct {
   1357 	Schema map[string][]string `json:"builder_name_schema"`
   1358 	Sep    string              `json:"builder_name_sep"`
   1359 }
   1360 
   1361 // NewJobNameSchema returns a JobNameSchema instance based on the given JSON
   1362 // file.
   1363 func NewJobNameSchema(jsonFile string) (*JobNameSchema, error) {
   1364 	var rv JobNameSchema
   1365 	f, err := os.Open(jsonFile)
   1366 	if err != nil {
   1367 		return nil, err
   1368 	}
   1369 	defer util.Close(f)
   1370 	if err := json.NewDecoder(f).Decode(&rv); err != nil {
   1371 		return nil, err
   1372 	}
   1373 	return &rv, nil
   1374 }
   1375 
   1376 // ParseJobName splits the given Job name into its component parts, according
   1377 // to the schema.
   1378 func (s *JobNameSchema) ParseJobName(n string) (map[string]string, error) {
   1379 	split := strings.Split(n, s.Sep)
   1380 	if len(split) < 2 {
   1381 		return nil, fmt.Errorf("Invalid job name: %q", n)
   1382 	}
   1383 	role := split[0]
   1384 	split = split[1:]
   1385 	keys, ok := s.Schema[role]
   1386 	if !ok {
   1387 		return nil, fmt.Errorf("Invalid job name; %q is not a valid role.", role)
   1388 	}
   1389 	extraConfig := ""
   1390 	if len(split) == len(keys)+1 {
   1391 		extraConfig = split[len(split)-1]
   1392 		split = split[:len(split)-1]
   1393 	}
   1394 	if len(split) != len(keys) {
   1395 		return nil, fmt.Errorf("Invalid job name; %q has incorrect number of parts.", n)
   1396 	}
   1397 	rv := make(map[string]string, len(keys)+2)
   1398 	rv["role"] = role
   1399 	if extraConfig != "" {
   1400 		rv["extra_config"] = extraConfig
   1401 	}
   1402 	for i, k := range keys {
   1403 		rv[k] = split[i]
   1404 	}
   1405 	return rv, nil
   1406 }
   1407 
   1408 // MakeJobName assembles the given parts of a Job name, according to the schema.
   1409 func (s *JobNameSchema) MakeJobName(parts map[string]string) (string, error) {
   1410 	role, ok := parts["role"]
   1411 	if !ok {
   1412 		return "", fmt.Errorf("Invalid job parts; jobs must have a role.")
   1413 	}
   1414 	keys, ok := s.Schema[role]
   1415 	if !ok {
   1416 		return "", fmt.Errorf("Invalid job parts; unknown role %q", role)
   1417 	}
   1418 	rvParts := make([]string, 0, len(parts))
   1419 	rvParts = append(rvParts, role)
   1420 	for _, k := range keys {
   1421 		v, ok := parts[k]
   1422 		if !ok {
   1423 			return "", fmt.Errorf("Invalid job parts; missing %q", k)
   1424 		}
   1425 		rvParts = append(rvParts, v)
   1426 	}
   1427 	if _, ok := parts["extra_config"]; ok {
   1428 		rvParts = append(rvParts, parts["extra_config"])
   1429 	}
   1430 	return strings.Join(rvParts, s.Sep), nil
   1431 }
   1432