build: Use context var instead of global state

Use a context variable to store build details instead of keeping
everything as members of the build module.

Also, fix a bug where archive_to_cas() didn't return anything, and
change the default build root directory to be inside the checkout root.

Change-Id: I83784fc381c879dd3466ec93995539a130db35de
Reviewed-on: https://pigweed-review.googlesource.com/c/infra/recipes/+/135395
Reviewed-by: Ted Pudlik <tpudlik@google.com>
Commit-Queue: Rob Mohr <mohrr@google.com>
diff --git a/recipe_modules/build/api.py b/recipe_modules/build/api.py
index d6e898f..40f47c4 100644
--- a/recipe_modules/build/api.py
+++ b/recipe_modules/build/api.py
@@ -13,39 +13,50 @@
 # the License.
 """Calls to build code."""
 
+import attr
 from recipe_engine import recipe_api
 
 
+@attr.s
+class Context:
+    _api = attr.ib()
+    checkout_root = attr.ib()
+    root = attr.ib()
+    options = attr.ib()
+
+
 class BuildApi(recipe_api.RecipeApi):
     """Calls to build code."""
 
     CAS_DIGEST_PROPERTY_NAME = 'cas_build_digest'
 
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        self.dir = None
+    def create(self, checkout_root, options, root=None):
+        if not root:
+            root = checkout_root.join('out')
+        return Context(self.m, checkout_root, root, options)
 
-    def initialize(self):
-        self.dir = self.m.path['start_dir'].join('build')
+    def __call__(self, ctx):
+        self.gn_gen(ctx)
+        self.ninja(ctx)
 
-    def gn_gen(self, checkout_dir, options):
+    def gn_gen(self, ctx):
         cmd = ['gn', 'gen']
 
-        for gn_arg in options.gn_args:
+        for gn_arg in ctx.options.gn_args:
             cmd.append(f'--args={gn_arg}')
 
         # Infrequently needed but harmless to always add this.
         cmd.append('--export-compile-commands')
 
-        cmd.append(self.dir)
+        cmd.append(ctx.root)
 
-        with self.m.context(cwd=checkout_dir):
+        with self.m.context(cwd=ctx.checkout_root):
             self.m.step('gn gen', cmd)
 
-    def get_gn_args(self, checkout_root=None, test_data=None):
-        context_kwargs = {'cwd': checkout_root} if checkout_root else {}
+    def get_gn_args(self, ctx, test_data=None):
+        context_kwargs = {'cwd': ctx.checkout_root} if ctx.checkout_root else {}
         with self.m.context(**context_kwargs):
-            cmd = ['gn', 'args', self.dir, '--list', '--json']
+            cmd = ['gn', 'args', ctx.root, '--list', '--json']
             args = self.m.step(
                 'all gn args',
                 cmd,
@@ -56,24 +67,21 @@
             ).stdout
             return {x['name']: x for x in args or ()}
 
-    def ninja(self, options):
-        cmd = ['ninja', '-C', self.dir]
-        cmd.extend(options.ninja_targets)
+    def ninja(self, ctx):
+        cmd = ['ninja', '-C', ctx.root]
+        cmd.extend(ctx.options.ninja_targets)
         with self.m.default_timeout():
             self.m.step('ninja', cmd)
 
-    def __call__(self, checkout_dir, options):
-        self.gn_gen(checkout_dir, options)
-        self.ninja(options)
-
-    def archive_to_cas(self):
+    def archive_to_cas(self, ctx):
         # TODO(b/234879756) Only archive necessary files.
         with self.m.step.nest('archive to cas') as pres:
-            digest = self.m.cas.archive('archive', self.dir, self.dir)
+            digest = self.m.cas.archive('archive', ctx.root, ctx.root)
             pres.properties[self.CAS_DIGEST_PROPERTY_NAME] = digest
+            return digest
 
-    def download_from_cas(self, digest):
-        return self.m.cas.download('download from cas', digest, self.dir)
+    def download_from_cas(self, ctx, digest):
+        return self.m.cas.download('download from cas', digest, ctx.root)
 
     def log_longest_build_steps(self, ninja_log):
         """Parse the build log and log the longest-running build steps."""
@@ -107,9 +115,6 @@
         log_longest_build_steps() on it.
         """
 
-        if build_dir is None:
-            build_dir = self.dir
-
         globs = [
             '*.gn',
             '*.log',
diff --git a/recipe_modules/build/tests/full.expected/full.json b/recipe_modules/build/tests/full.expected/full.json
index b305558..e18dbad 100644
--- a/recipe_modules/build/tests/full.expected/full.json
+++ b/recipe_modules/build/tests/full.expected/full.json
@@ -5,7 +5,7 @@
       "gen",
       "--args=foo=true",
       "--export-compile-commands",
-      "[START_DIR]/build"
+      "[START_DIR]/checkout/out"
     ],
     "cwd": "[START_DIR]/checkout",
     "name": "gn gen"
@@ -21,7 +21,7 @@
     "cmd": [
       "ninja",
       "-C",
-      "[START_DIR]/build",
+      "[START_DIR]/checkout/out",
       "target"
     ],
     "luci_context": {
@@ -62,7 +62,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "glob",
-      "[START_DIR]/build",
+      "[START_DIR]/checkout/out",
       "*.gn",
       "--hidden"
     ],
@@ -81,7 +81,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "glob",
-      "[START_DIR]/build",
+      "[START_DIR]/checkout/out",
       "*.log",
       "--hidden"
     ],
@@ -89,9 +89,9 @@
     "name": "save logs.logs.glob.*.log",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@3@@@",
-      "@@@STEP_LOG_LINE@glob@[START_DIR]/build/.ninja_log@@@",
-      "@@@STEP_LOG_LINE@glob@[START_DIR]/build/failure-summary.log@@@",
-      "@@@STEP_LOG_LINE@glob@[START_DIR]/build/links.json@@@",
+      "@@@STEP_LOG_LINE@glob@[START_DIR]/checkout/out/.ninja_log@@@",
+      "@@@STEP_LOG_LINE@glob@[START_DIR]/checkout/out/failure-summary.log@@@",
+      "@@@STEP_LOG_LINE@glob@[START_DIR]/checkout/out/links.json@@@",
       "@@@STEP_LOG_END@glob@@@"
     ]
   },
@@ -103,7 +103,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "glob",
-      "[START_DIR]/build",
+      "[START_DIR]/checkout/out",
       "*.json",
       "--hidden"
     ],
@@ -122,7 +122,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "glob",
-      "[START_DIR]/build",
+      "[START_DIR]/checkout/out",
       "*.compdb",
       "--hidden"
     ],
@@ -141,7 +141,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "glob",
-      "[START_DIR]/build",
+      "[START_DIR]/checkout/out",
       "*.graph",
       "--hidden"
     ],
@@ -160,7 +160,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "glob",
-      "[START_DIR]/build",
+      "[START_DIR]/checkout/out",
       "*_log",
       "--hidden"
     ],
@@ -179,7 +179,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "[START_DIR]/build/.ninja_log",
+      "[START_DIR]/checkout/out/.ninja_log",
       "/path/to/tmp/"
     ],
     "infra_step": true,
@@ -202,7 +202,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "[START_DIR]/build/failure-summary.log",
+      "[START_DIR]/checkout/out/failure-summary.log",
       "/path/to/tmp/"
     ],
     "infra_step": true,
@@ -222,7 +222,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "[START_DIR]/build/links.json",
+      "[START_DIR]/checkout/out/links.json",
       "/path/to/tmp/"
     ],
     "infra_step": true,
@@ -291,7 +291,7 @@
       "ensure-directory",
       "--mode",
       "0777",
-      "[START_DIR]/build/export/build_logs"
+      "[START_DIR]/checkout/out/export/build_logs"
     ],
     "infra_step": true,
     "name": "save logs.mkdir build_logs",
@@ -314,8 +314,8 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "[START_DIR]/build/.ninja_log",
-      "[START_DIR]/build/export/build_logs/.ninja_log"
+      "[START_DIR]/checkout/out/.ninja_log",
+      "[START_DIR]/checkout/out/export/build_logs/.ninja_log"
     ],
     "infra_step": true,
     "name": "save logs.copy..ninja_log",
@@ -331,8 +331,8 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "[START_DIR]/build/failure-summary.log",
-      "[START_DIR]/build/export/build_logs/failure-summary.log"
+      "[START_DIR]/checkout/out/failure-summary.log",
+      "[START_DIR]/checkout/out/export/build_logs/failure-summary.log"
     ],
     "infra_step": true,
     "name": "save logs.copy.failure-summary.log",
@@ -348,8 +348,8 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "[START_DIR]/build/links.json",
-      "[START_DIR]/build/export/build_logs/links.json"
+      "[START_DIR]/checkout/out/links.json",
+      "[START_DIR]/checkout/out/export/build_logs/links.json"
     ],
     "infra_step": true,
     "name": "save logs.copy.links.json",
@@ -361,10 +361,11 @@
     "cmd": [
       "gn",
       "args",
-      "[START_DIR]/build",
+      "[START_DIR]/checkout/out",
       "--list",
       "--json"
     ],
+    "cwd": "[START_DIR]/checkout",
     "name": "all gn args",
     "~followup_annotations": [
       "@@@STEP_LOG_LINE@json.output@[]@@@",
@@ -461,7 +462,7 @@
       "-dump-digest",
       "/path/to/tmp/",
       "-paths-json",
-      "[[\"[START_DIR]/build\", \".\"]]"
+      "[[\"[START_DIR]/checkout/out\", \".\"]]"
     ],
     "infra_step": true,
     "name": "archive to cas.archive",
@@ -479,7 +480,7 @@
       "-digest",
       "digest",
       "-dir",
-      "[START_DIR]/build"
+      "[START_DIR]/checkout/out"
     ],
     "infra_step": true,
     "name": "download from cas"
diff --git a/recipe_modules/build/tests/full.py b/recipe_modules/build/tests/full.py
index f1e0647..ed2863a 100644
--- a/recipe_modules/build/tests/full.py
+++ b/recipe_modules/build/tests/full.py
@@ -26,12 +26,19 @@
 
 
 def RunSteps(api, props):
-    api.build(api.path['start_dir'].join('checkout'), props.build_options)
+    build = api.build.create(
+        api.path['start_dir'].join('checkout'), props.build_options,
+    )
+    api.build(build)
     with api.step.nest('save logs') as pres:
-        api.build.save_logs(export_dir=api.build.dir.join('export'), pres=pres)
-    api.build.get_gn_args()
-    api.build.archive_to_cas()
-    api.build.download_from_cas('digest')
+        api.build.save_logs(
+            build_dir=build.root,
+            export_dir=build.root.join('export'),
+            pres=pres,
+        )
+    api.build.get_gn_args(build)
+    api.build.archive_to_cas(build)
+    api.build.download_from_cas(build, 'digest')
 
 
 def GenTests(api):  # pylint: disable=invalid-name
diff --git a/recipe_modules/environment/__init__.py b/recipe_modules/environment/__init__.py
index 2c15d03..5684b7c 100644
--- a/recipe_modules/environment/__init__.py
+++ b/recipe_modules/environment/__init__.py
@@ -16,7 +16,6 @@
 
 DEPS = [
     'fuchsia/macos_sdk',
-    'pigweed/build',
     'pigweed/default_timeout',
     'recipe_engine/buildbucket',
     'recipe_engine/cas',
diff --git a/recipe_modules/environment/api.py b/recipe_modules/environment/api.py
index 468ae91..d049889 100644
--- a/recipe_modules/environment/api.py
+++ b/recipe_modules/environment/api.py
@@ -149,7 +149,7 @@
             '--shell-file',
             shell_file,
             '--virtualenv-gn-out-dir',
-            self.m.build.dir,
+            env.dir.join('out'),
             '--use-existing-cipd',
             '--strict',
         ]
diff --git a/recipe_modules/environment/tests/full.expected/doctor-fail.json b/recipe_modules/environment/tests/full.expected/doctor-fail.json
index fe14c29..f2a504c 100644
--- a/recipe_modules/environment/tests/full.expected/doctor-fail.json
+++ b/recipe_modules/environment/tests/full.expected/doctor-fail.json
@@ -97,7 +97,7 @@
       "--shell-file",
       "[START_DIR]/environment/setup.sh",
       "--virtualenv-gn-out-dir",
-      "[START_DIR]/build",
+      "[START_DIR]/environment/out",
       "--use-existing-cipd",
       "--strict",
       "--skip-submodule-check",
diff --git a/recipe_modules/environment/tests/full.expected/normal.json b/recipe_modules/environment/tests/full.expected/normal.json
index 6ffd1bb..361ab55 100644
--- a/recipe_modules/environment/tests/full.expected/normal.json
+++ b/recipe_modules/environment/tests/full.expected/normal.json
@@ -291,7 +291,7 @@
       "--shell-file",
       "[START_DIR]/environment/setup.sh",
       "--virtualenv-gn-out-dir",
-      "[START_DIR]/build",
+      "[START_DIR]/environment/out",
       "--use-existing-cipd",
       "--strict",
       "--skip-submodule-check",
diff --git a/recipe_modules/environment/tests/full.expected/override-cas.json b/recipe_modules/environment/tests/full.expected/override-cas.json
index 6138d27..2837bf3 100644
--- a/recipe_modules/environment/tests/full.expected/override-cas.json
+++ b/recipe_modules/environment/tests/full.expected/override-cas.json
@@ -96,7 +96,7 @@
       "--shell-file",
       "[START_DIR]/environment/setup.sh",
       "--virtualenv-gn-out-dir",
-      "[START_DIR]/build",
+      "[START_DIR]/environment/out",
       "--use-existing-cipd",
       "--strict",
       "--skip-submodule-check",
diff --git a/recipe_modules/environment/tests/full.expected/override-cipd.json b/recipe_modules/environment/tests/full.expected/override-cipd.json
index 3275b43..31e3165 100644
--- a/recipe_modules/environment/tests/full.expected/override-cipd.json
+++ b/recipe_modules/environment/tests/full.expected/override-cipd.json
@@ -96,7 +96,7 @@
       "--shell-file",
       "[START_DIR]/environment/setup.sh",
       "--virtualenv-gn-out-dir",
-      "[START_DIR]/build",
+      "[START_DIR]/environment/out",
       "--use-existing-cipd",
       "--strict",
       "--skip-submodule-check",
diff --git a/recipe_modules/environment/tests/full.expected/windows.json b/recipe_modules/environment/tests/full.expected/windows.json
index 0630d7b..cd43c2e 100644
--- a/recipe_modules/environment/tests/full.expected/windows.json
+++ b/recipe_modules/environment/tests/full.expected/windows.json
@@ -96,7 +96,7 @@
       "--shell-file",
       "[START_DIR]\\environment\\setup.sh",
       "--virtualenv-gn-out-dir",
-      "[START_DIR]\\build",
+      "[START_DIR]\\environment\\out",
       "--use-existing-cipd",
       "--strict",
       "--unpin-pip-packages",
diff --git a/recipes/build.expected/basic.json b/recipes/build.expected/basic.json
index f642d1a..65005a0 100644
--- a/recipes/build.expected/basic.json
+++ b/recipes/build.expected/basic.json
@@ -1504,7 +1504,7 @@
       "gen",
       "--args=gnarg",
       "--export-compile-commands",
-      "[START_DIR]/build"
+      "[START_DIR]/co/out"
     ],
     "cwd": "[START_DIR]/co",
     "env": {
@@ -1535,7 +1535,7 @@
     "cmd": [
       "ninja",
       "-C",
-      "[START_DIR]/build"
+      "[START_DIR]/co/out"
     ],
     "env": {
       "PW_TEST_VAR": "test_value"
@@ -1693,7 +1693,7 @@
       "-dump-digest",
       "/path/to/tmp/",
       "-paths-json",
-      "[[\"[START_DIR]/build\", \".\"]]"
+      "[[\"[START_DIR]/co/out\", \".\"]]"
     ],
     "env": {
       "PW_TEST_VAR": "test_value"
@@ -1721,7 +1721,7 @@
     "cmd": [],
     "name": "result",
     "~followup_annotations": [
-      "@@@SET_BUILD_PROPERTY@digest@null@@@"
+      "@@@SET_BUILD_PROPERTY@digest@\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855/0\"@@@"
     ]
   },
   {
diff --git a/recipes/build.expected/skip_archive.json b/recipes/build.expected/skip_archive.json
index b328ad7..b9381d8 100644
--- a/recipes/build.expected/skip_archive.json
+++ b/recipes/build.expected/skip_archive.json
@@ -1504,7 +1504,7 @@
       "gen",
       "--args=gnarg",
       "--export-compile-commands",
-      "[START_DIR]/build"
+      "[START_DIR]/co/out"
     ],
     "cwd": "[START_DIR]/co",
     "env": {
@@ -1535,7 +1535,7 @@
     "cmd": [
       "ninja",
       "-C",
-      "[START_DIR]/build"
+      "[START_DIR]/co/out"
     ],
     "env": {
       "PW_TEST_VAR": "test_value"
@@ -1559,6 +1559,10 @@
     "name": "ninja"
   },
   {
+    "cmd": [],
+    "name": "result"
+  },
+  {
     "name": "$result"
   }
 ]
\ No newline at end of file
diff --git a/recipes/build.py b/recipes/build.py
index 1a3425c..1e14680 100644
--- a/recipes/build.py
+++ b/recipes/build.py
@@ -29,15 +29,17 @@
 def RunSteps(api, props):
     checkout = api.checkout(props.checkout_options)
     env = api.environment.init(checkout, props.environment_options)
+    digest = None
 
     with env():
-        api.build(checkout.root, props.build_options)
-        if props.skip_archive:
-            return
-        digest = api.build.archive_to_cas()
+        build = api.build.create(checkout.root, props.build_options)
+        api.build(build)
+        if not props.skip_archive:
+            digest = api.build.archive_to_cas(build)
 
     with api.step.nest('result') as pres:
-        pres.properties['digest'] = digest
+        if digest:
+            pres.properties['digest'] = digest
 
 
 def GenTests(api):
diff --git a/recipes/docs_builder.expected/docs.json b/recipes/docs_builder.expected/docs.json
index ae4219c..7bdbe94 100644
--- a/recipes/docs_builder.expected/docs.json
+++ b/recipes/docs_builder.expected/docs.json
@@ -1018,7 +1018,7 @@
       "gn",
       "gen",
       "--export-compile-commands",
-      "[START_DIR]/build"
+      "[START_DIR]/co/out"
     ],
     "cwd": "[START_DIR]/co",
     "env": {
@@ -1037,7 +1037,7 @@
     "cmd": [
       "ninja",
       "-C",
-      "[START_DIR]/build"
+      "[START_DIR]/co/out"
     ],
     "env": {
       "PW_TEST_VAR": "test_value"
@@ -1155,7 +1155,7 @@
       "-m",
       "cp",
       "-r",
-      "[START_DIR]/build/docs/gen/docs/html",
+      "[START_DIR]/co/out/docs/gen/docs/html",
       "gs://pigweed-docs/HASH"
     ],
     "infra_step": true,
@@ -1199,7 +1199,7 @@
       "/path/to/tmp/json",
       "copy",
       "HASH",
-      "[START_DIR]/build/HEAD"
+      "[START_DIR]/co/out/HEAD"
     ],
     "infra_step": true,
     "name": "write HEAD",
@@ -1216,7 +1216,7 @@
       "-o",
       "GSUtil:software_update_check_period=0",
       "cp",
-      "[START_DIR]/build/HEAD",
+      "[START_DIR]/co/out/HEAD",
       "gs://pigweed-docs/HEAD"
     ],
     "infra_step": true,
@@ -1235,7 +1235,7 @@
       "-m",
       "rsync",
       "-r",
-      "[START_DIR]/build/docs/gen/docs/html",
+      "[START_DIR]/co/out/docs/gen/docs/html",
       "gs://pigweed-docs/latest"
     ],
     "infra_step": true,
diff --git a/recipes/docs_builder.expected/docs_dry_run.json b/recipes/docs_builder.expected/docs_dry_run.json
index a0e49f8..275bad6 100644
--- a/recipes/docs_builder.expected/docs_dry_run.json
+++ b/recipes/docs_builder.expected/docs_dry_run.json
@@ -1018,7 +1018,7 @@
       "gn",
       "gen",
       "--export-compile-commands",
-      "[START_DIR]/build"
+      "[START_DIR]/co/out"
     ],
     "cwd": "[START_DIR]/co",
     "env": {
@@ -1037,7 +1037,7 @@
     "cmd": [
       "ninja",
       "-C",
-      "[START_DIR]/build"
+      "[START_DIR]/co/out"
     ],
     "env": {
       "PW_TEST_VAR": "test_value"
@@ -1155,7 +1155,7 @@
       "-m",
       "cp",
       "-r",
-      "[START_DIR]/build/docs/gen/docs/html",
+      "[START_DIR]/co/out/docs/gen/docs/html",
       "gs://pigweed-docs/testing/swarming-fake-task-id/HASH"
     ],
     "infra_step": true,
diff --git a/recipes/docs_builder.py b/recipes/docs_builder.py
index 504e1d0..0ff91c9 100644
--- a/recipes/docs_builder.py
+++ b/recipes/docs_builder.py
@@ -38,11 +38,12 @@
 
     checkout = api.checkout(props.checkout_options)
     env = api.environment.init(checkout, props.environment_options)
+    build = api.build.create(checkout.root, props.build_options)
 
     with env():
-        api.build(checkout.root, props.build_options)
+        api.build(build)
 
-    html = api.build.dir.join('docs', 'gen', 'docs', 'html')
+    html = build.root.join('docs', 'gen', 'docs', 'html')
 
     if props.dry_run:
         path = f'testing/swarming-{api.swarming.task_id}/{checkout.revision()}'
@@ -65,7 +66,7 @@
     ).json.output
 
     if change['branch'] in ('master', 'main') and not props.dry_run:
-        head = api.build.dir.join('HEAD')
+        head = build.root.join('HEAD')
         api.file.write_text('write HEAD', head, checkout.revision())
         api.gsutil.upload(bucket=bucket, src=head, dst='HEAD')
 
diff --git a/recipes/target_to_cipd.expected/success.json b/recipes/target_to_cipd.expected/success.json
index 41ab641..be4da78 100644
--- a/recipes/target_to_cipd.expected/success.json
+++ b/recipes/target_to_cipd.expected/success.json
@@ -1131,65 +1131,6 @@
   },
   {
     "cmd": [
-      "gn",
-      "gen",
-      "--export-compile-commands",
-      "[START_DIR]/build"
-    ],
-    "cwd": "[START_DIR]/co",
-    "env": {
-      "PW_TEST_VAR": "test_value"
-    },
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "gn gen"
-  },
-  {
-    "cmd": [],
-    "name": "timeout 11h 4m 34s",
-    "~followup_annotations": [
-      "@@@STEP_SUMMARY_TEXT@soft_deadline: -120.0@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "ninja",
-      "-C",
-      "[START_DIR]/build"
-    ],
-    "env": {
-      "PW_TEST_VAR": "test_value"
-    },
-    "luci_context": {
-      "deadline": {
-        "grace_period": 30.0,
-        "soft_deadline": -120.0
-      },
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "ninja"
-  },
-  {
-    "cmd": [
       "vpython3",
       "-u",
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
@@ -1227,7 +1168,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "glob",
-      "[START_DIR]/build",
+      "[START_DIR]/co/out",
       "foo/bar/baz"
     ],
     "infra_step": true,
@@ -1246,7 +1187,7 @@
     "name": "foo/bar/baz.glob",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_LOG_LINE@glob@[START_DIR]/build/foo/bar/baz@@@",
+      "@@@STEP_LOG_LINE@glob@[START_DIR]/co/out/foo/bar/baz@@@",
       "@@@STEP_LOG_END@glob@@@"
     ]
   },
@@ -1255,7 +1196,7 @@
     "name": "foo/bar/baz.source",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@[START_DIR]/build/foo/bar/baz\n[START_DIR]/build\n[START_DIR]/cipd-package/foo/replacement/baz@@@"
+      "@@@STEP_SUMMARY_TEXT@[START_DIR]/co/out/foo/bar/baz\n[START_DIR]/co/out\n[START_DIR]/cipd-package/foo/replacement/baz@@@"
     ]
   },
   {
@@ -1296,7 +1237,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "[START_DIR]/build/foo/bar/baz",
+      "[START_DIR]/co/out/foo/bar/baz",
       "[START_DIR]/cipd-package/foo/replacement/baz"
     ],
     "infra_step": true,
@@ -1312,7 +1253,7 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "foo/bar/baz.copy [START_DIR]/build/foo/bar/baz [START_DIR]/cipd-package/foo/replacement/baz",
+    "name": "foo/bar/baz.copy [START_DIR]/co/out/foo/bar/baz [START_DIR]/cipd-package/foo/replacement/baz",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@1@@@"
     ]
@@ -1516,7 +1457,7 @@
       }
     },
     "name": "trigger ROLL",
-    "stdin": "{\"batches\": [{\"jobs\": [{\"job\": \"ROLL\", \"project\": \"project\"}], \"trigger\": {\"gitiles\": {\"ref\": \"refs/heads/main\", \"repo\": \"https://pigweed.googlesource.com/pigweed/manifest\", \"revision\": \"2d72510e447ab60a9728aeea2362d8be2cbd7789\", \"tags\": [\"parent_buildername:builder\", \"user_agent:recipe\"]}, \"id\": \"6a0a73b0-070b-492b-9135-9f26a2a00001\", \"title\": \"builder/0\"}}], \"timestamp\": 1337000007500000}",
+    "stdin": "{\"batches\": [{\"jobs\": [{\"job\": \"ROLL\", \"project\": \"project\"}], \"trigger\": {\"gitiles\": {\"ref\": \"refs/heads/main\", \"repo\": \"https://pigweed.googlesource.com/pigweed/manifest\", \"revision\": \"2d72510e447ab60a9728aeea2362d8be2cbd7789\", \"tags\": [\"parent_buildername:builder\", \"user_agent:recipe\"]}, \"id\": \"6a0a73b0-070b-492b-9135-9f26a2a00001\", \"title\": \"builder/0\"}}], \"timestamp\": 1337000006000000}",
     "~followup_annotations": [
       "@@@STEP_LOG_LINE@input@{@@@",
       "@@@STEP_LOG_LINE@input@    \"batches\": [@@@",
@@ -1542,7 +1483,7 @@
       "@@@STEP_LOG_LINE@input@            }@@@",
       "@@@STEP_LOG_LINE@input@        }@@@",
       "@@@STEP_LOG_LINE@input@    ], @@@",
-      "@@@STEP_LOG_LINE@input@    \"timestamp\": 1337000007500000@@@",
+      "@@@STEP_LOG_LINE@input@    \"timestamp\": 1337000006000000@@@",
       "@@@STEP_LOG_LINE@input@}@@@",
       "@@@STEP_LOG_END@input@@@"
     ]
diff --git a/recipes/target_to_cipd.py b/recipes/target_to_cipd.py
index f0fc41c..b5d3439 100644
--- a/recipes/target_to_cipd.py
+++ b/recipes/target_to_cipd.py
@@ -38,9 +38,8 @@
 
     with env():
         if props.artifacts:
-            api.build(checkout.root, props.build_options)
-            build_dir = api.build.dir
-            export_dir = build_dir.join(
+            build = api.build.create(checkout.root, props.build_options)
+            export_dir = build.root.join(
                 props.pw_presubmit_options.export_dir_name
             )
 
@@ -75,17 +74,17 @@
         for glob in props.artifacts:
             with api.step.nest(glob):
                 sources = api.file.glob_paths(
-                    'glob', api.build.dir, glob, test_data=(glob,)
+                    'glob', build.root, glob, test_data=(glob,)
                 )
                 if not sources:  # pragma: no cover
-                    api.file.listdir('ls build', api.build.dir, recursive=True)
+                    api.file.listdir('ls build', build.root, recursive=True)
                     raise api.step.StepFailure(f'no matches for {glob}')
                 for source in sources:
                     with api.step.nest('source') as pres:
                         pres.step_summary_text = '\n'.join(
-                            (str(source), str(api.build.dir))
+                            (str(source), str(build.root))
                         )
-                        relpath = api.path.relpath(source, api.build.dir)
+                        relpath = api.path.relpath(source, build.root)
                         for replacement in props.replacements:
                             relpath = relpath.replace(
                                 replacement.old, replacement.new
diff --git a/recipes/tokendb_updater.expected/dry-run.json b/recipes/tokendb_updater.expected/dry-run.json
index ea7a9b6..9e45551 100644
--- a/recipes/tokendb_updater.expected/dry-run.json
+++ b/recipes/tokendb_updater.expected/dry-run.json
@@ -1018,7 +1018,7 @@
       "gn",
       "gen",
       "--export-compile-commands",
-      "[START_DIR]/build"
+      "[START_DIR]/co/out"
     ],
     "cwd": "[START_DIR]/co",
     "env": {
@@ -1037,7 +1037,7 @@
     "cmd": [
       "ninja",
       "-C",
-      "[START_DIR]/build"
+      "[START_DIR]/co/out"
     ],
     "env": {
       "PW_TEST_VAR": "test_value"
diff --git a/recipes/tokendb_updater.expected/separate-repo.json b/recipes/tokendb_updater.expected/separate-repo.json
index bd59f51..a21d320 100644
--- a/recipes/tokendb_updater.expected/separate-repo.json
+++ b/recipes/tokendb_updater.expected/separate-repo.json
@@ -1555,7 +1555,7 @@
       "gn",
       "gen",
       "--export-compile-commands",
-      "[START_DIR]/build"
+      "[START_DIR]/co/out"
     ],
     "cwd": "[START_DIR]/co",
     "env": {
@@ -1574,7 +1574,7 @@
     "cmd": [
       "ninja",
       "-C",
-      "[START_DIR]/build"
+      "[START_DIR]/co/out"
     ],
     "env": {
       "PW_TEST_VAR": "test_value"
diff --git a/recipes/tokendb_updater.expected/simple.json b/recipes/tokendb_updater.expected/simple.json
index 2adb5b7..0286292 100644
--- a/recipes/tokendb_updater.expected/simple.json
+++ b/recipes/tokendb_updater.expected/simple.json
@@ -1018,7 +1018,7 @@
       "gn",
       "gen",
       "--export-compile-commands",
-      "[START_DIR]/build"
+      "[START_DIR]/co/out"
     ],
     "cwd": "[START_DIR]/co",
     "env": {
@@ -1037,7 +1037,7 @@
     "cmd": [
       "ninja",
       "-C",
-      "[START_DIR]/build"
+      "[START_DIR]/co/out"
     ],
     "env": {
       "PW_TEST_VAR": "test_value"
diff --git a/recipes/tokendb_updater.py b/recipes/tokendb_updater.py
index a531b31..552adb9 100644
--- a/recipes/tokendb_updater.py
+++ b/recipes/tokendb_updater.py
@@ -95,9 +95,10 @@
         )
 
     env = api.environment.init(checkout, props.environment_options)
+    build = api.build.create(checkout.root, props.build_options)
 
     with env():
-        api.build(checkout.root, props.build_options)
+        api.build(build)
 
     tokendb_path = tokendb_repo.join(*re.split(r'/+', props.tokendb_path))
 
diff --git a/recipes/update_python_versions.expected/simple.json b/recipes/update_python_versions.expected/simple.json
index 74aff94..427a455 100644
--- a/recipes/update_python_versions.expected/simple.json
+++ b/recipes/update_python_versions.expected/simple.json
@@ -1018,7 +1018,7 @@
       "gn",
       "gen",
       "--export-compile-commands",
-      "[START_DIR]/build"
+      "[START_DIR]/co/out"
     ],
     "cwd": "[START_DIR]/co",
     "env": {
@@ -1030,7 +1030,7 @@
     "cmd": [
       "gn",
       "args",
-      "[START_DIR]/build",
+      "[START_DIR]/co/out",
       "--list",
       "--json"
     ],
diff --git a/recipes/update_python_versions.py b/recipes/update_python_versions.py
index f92b403..0137e2d 100644
--- a/recipes/update_python_versions.py
+++ b/recipes/update_python_versions.py
@@ -40,13 +40,14 @@
     with env():
         constraint_file = props.path_to_constraint_file
         if not constraint_file:
-            api.build.gn_gen(checkout.root, props.build_options)
+            build = api.build.create(checkout.root, props.build_options)
+            api.build.gn_gen(build)
 
             # pw_build_PIP_CONSTRAINTS is a GN arg that points to a pip
             # constraint file. For more see
             # https://pigweed.googlesource.com/pigweed/pigweed/+/main/pw_build/python.gni
             gn_args = api.build.get_gn_args(
-                checkout_root=checkout.root,
+                build,
                 test_data=[
                     {
                         'name': 'pw_build_PIP_CONSTRAINTS',
diff --git a/recipes/xrefs.expected/dry_run.json b/recipes/xrefs.expected/dry_run.json
index 4196511..0c1e921 100644
--- a/recipes/xrefs.expected/dry_run.json
+++ b/recipes/xrefs.expected/dry_run.json
@@ -1018,7 +1018,7 @@
       "gn",
       "gen",
       "--export-compile-commands",
-      "[START_DIR]/build"
+      "[START_DIR]/co/out"
     ],
     "cwd": "[START_DIR]/co",
     "env": {
@@ -1062,9 +1062,9 @@
       "-extractor",
       "[START_DIR]/kythe/extractors/cxx_extractor",
       "-path",
-      "[START_DIR]/build/compile_commands.json"
+      "[START_DIR]/co/out/compile_commands.json"
     ],
-    "cwd": "[START_DIR]/build",
+    "cwd": "[START_DIR]/co/out",
     "env": {
       "KYTHE_CORPUS": "pigweed.googlesource.com/pigweed/pigweed",
       "KYTHE_OUTPUT_DIRECTORY": "[START_DIR]/kythe-output",
diff --git a/recipes/xrefs.expected/kythe.json b/recipes/xrefs.expected/kythe.json
index baf578b..d3cda06 100644
--- a/recipes/xrefs.expected/kythe.json
+++ b/recipes/xrefs.expected/kythe.json
@@ -1018,7 +1018,7 @@
       "gn",
       "gen",
       "--export-compile-commands",
-      "[START_DIR]/build"
+      "[START_DIR]/co/out"
     ],
     "cwd": "[START_DIR]/co",
     "env": {
@@ -1062,9 +1062,9 @@
       "-extractor",
       "[START_DIR]/kythe/extractors/cxx_extractor",
       "-path",
-      "[START_DIR]/build/compile_commands.json"
+      "[START_DIR]/co/out/compile_commands.json"
     ],
-    "cwd": "[START_DIR]/build",
+    "cwd": "[START_DIR]/co/out",
     "env": {
       "KYTHE_CORPUS": "pigweed.googlesource.com/pigweed/pigweed",
       "KYTHE_OUTPUT_DIRECTORY": "[START_DIR]/kythe-output",
diff --git a/recipes/xrefs.expected/tryjob.json b/recipes/xrefs.expected/tryjob.json
index 09c606e..2f3d742 100644
--- a/recipes/xrefs.expected/tryjob.json
+++ b/recipes/xrefs.expected/tryjob.json
@@ -1768,7 +1768,7 @@
       "gn",
       "gen",
       "--export-compile-commands",
-      "[START_DIR]/build"
+      "[START_DIR]/co/out"
     ],
     "cwd": "[START_DIR]/co",
     "env": {
@@ -1848,9 +1848,9 @@
       "-extractor",
       "[START_DIR]/kythe/extractors/cxx_extractor",
       "-path",
-      "[START_DIR]/build/compile_commands.json"
+      "[START_DIR]/co/out/compile_commands.json"
     ],
-    "cwd": "[START_DIR]/build",
+    "cwd": "[START_DIR]/co/out",
     "env": {
       "KYTHE_CORPUS": "pigweed.googlesource.com/pigweed/pigweed",
       "KYTHE_OUTPUT_DIRECTORY": "[START_DIR]/kythe-output",
diff --git a/recipes/xrefs.py b/recipes/xrefs.py
index b4e6f92..643a570 100644
--- a/recipes/xrefs.py
+++ b/recipes/xrefs.py
@@ -54,8 +54,10 @@
 
     api.kythe.kythe_libs_dir = api.kythe.kythe_dir
 
+    build = api.build.create(checkout.root, props.build_options)
+
     with env():
-        api.build.gn_gen(checkout.root, props.build_options)
+        api.build.gn_gen(build)
 
     assert checkout.options.branch in ('master', 'main')
     url = urllib.parse.urlparse(checkout.options.remote)
@@ -75,7 +77,7 @@
 
     api.kythe.extract_and_upload(
         checkout_dir=checkout.root,
-        build_dir=api.build.dir,
+        build_dir=build.root,
         corpus=corpus,
         gcs_bucket=gcs_bucket,
         gcs_filename=final_kzip_name,