The envtest recipe now works for Windows

Changes required to get the envtest recipe to work on Windows.
(Mostly a reland of pwrev/6740.)

Simplified way of setting PW_ROOT since then because of pwrev/6783.

Tested with led:

$ led get-builder luci.pigweed.ci:pigweed-windows-envtest \
    | led edit-recipe-bundle \
    | led launch
...
LUCI UI: https://ci.chromium.org/swarming/task/4a82d9f67accc310?server=chrome-swarming.appspot.com

$ led get-builder luci.pigweed.ci:pigweed-linux-envtest \
    | led edit-recipe-bundle \
    | led launch
...
LUCI UI: https://ci.chromium.org/swarming/task/4a82da0ab342c810?server=chrome-swarming.appspot.com

Bug: 92
Change-Id: I9a021ce2d7c7040ad37e088ad8ff7d619dc1f573
diff --git a/recipes/envtest.expected/fail.json b/recipes/envtest.expected/fail.json
index 2e4075e..d9c5bae 100644
--- a/recipes/envtest.expected/fail.json
+++ b/recipes/envtest.expected/fail.json
@@ -283,17 +283,13 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
-      "copy",
-      ". \"[START_DIR]/checkout/pw_env_setup/bootstrap.sh\"\npw --loglevel debug presubmit --step gn_clang_build --repository $PW_ROOT\n",
-      "[START_DIR]/run.sh"
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[START_DIR]/run"
     ],
     "infra_step": true,
-    "name": "write run.sh",
-    "~followup_annotations": [
-      "@@@STEP_LOG_LINE@run.sh@. \"[START_DIR]/checkout/pw_env_setup/bootstrap.sh\"@@@",
-      "@@@STEP_LOG_LINE@run.sh@pw --loglevel debug presubmit --step gn_clang_build --repository $PW_ROOT@@@",
-      "@@@STEP_LOG_END@run.sh@@@"
-    ]
+    "name": "mkdir run"
   },
   {
     "cmd": [
@@ -302,13 +298,17 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
-      "ensure-directory",
-      "--mode",
-      "0777",
-      "[START_DIR]/run"
+      "copy",
+      ". [START_DIR]/checkout/pw_env_setup/env_setup.sh\npw --loglevel debug presubmit --step gn_clang_build --repository $PW_ROOT\n",
+      "[START_DIR]/run.sh"
     ],
     "infra_step": true,
-    "name": "mkdir run"
+    "name": "write run.sh",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@run.sh@. [START_DIR]/checkout/pw_env_setup/env_setup.sh@@@",
+      "@@@STEP_LOG_LINE@run.sh@pw --loglevel debug presubmit --step gn_clang_build --repository $PW_ROOT@@@",
+      "@@@STEP_LOG_END@run.sh@@@"
+    ]
   },
   {
     "cmd": [
@@ -326,9 +326,26 @@
     ]
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "glob",
+      "[START_DIR]/checkout/pw_env_setup",
+      ".env_setup*"
+    ],
+    "infra_step": true,
+    "name": "glob pw_env_setup/.env_setup*",
+    "~followup_annotations": [
+      "@@@STEP_LOG_END@glob@@@"
+    ]
+  },
+  {
     "failure": {
       "failure": {},
-      "humanReason": "Step('run.sh') (retcode: 1)"
+      "humanReason": "1 out of 2 aggregated steps failed: Step('run.sh') (retcode: 1)"
     },
     "name": "$result"
   }
diff --git a/recipes/envtest.expected/pigweed.json b/recipes/envtest.expected/pigweed.json
index 309b775..9635e25 100644
--- a/recipes/envtest.expected/pigweed.json
+++ b/recipes/envtest.expected/pigweed.json
@@ -283,17 +283,13 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
-      "copy",
-      ". \"[START_DIR]/checkout/pw_env_setup/bootstrap.sh\"\npw --loglevel debug presubmit --step gn_clang_build --repository $PW_ROOT\n",
-      "[START_DIR]/run.sh"
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[START_DIR]/run"
     ],
     "infra_step": true,
-    "name": "write run.sh",
-    "~followup_annotations": [
-      "@@@STEP_LOG_LINE@run.sh@. \"[START_DIR]/checkout/pw_env_setup/bootstrap.sh\"@@@",
-      "@@@STEP_LOG_LINE@run.sh@pw --loglevel debug presubmit --step gn_clang_build --repository $PW_ROOT@@@",
-      "@@@STEP_LOG_END@run.sh@@@"
-    ]
+    "name": "mkdir run"
   },
   {
     "cmd": [
@@ -302,13 +298,17 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
-      "ensure-directory",
-      "--mode",
-      "0777",
-      "[START_DIR]/run"
+      "copy",
+      ". [START_DIR]/checkout/pw_env_setup/env_setup.sh\npw --loglevel debug presubmit --step gn_clang_build --repository $PW_ROOT\n",
+      "[START_DIR]/run.sh"
     ],
     "infra_step": true,
-    "name": "mkdir run"
+    "name": "write run.sh",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@run.sh@. [START_DIR]/checkout/pw_env_setup/env_setup.sh@@@",
+      "@@@STEP_LOG_LINE@run.sh@pw --loglevel debug presubmit --step gn_clang_build --repository $PW_ROOT@@@",
+      "@@@STEP_LOG_END@run.sh@@@"
+    ]
   },
   {
     "cmd": [
@@ -323,6 +323,23 @@
     "name": "run.sh"
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "glob",
+      "[START_DIR]/checkout/pw_env_setup",
+      ".env_setup*"
+    ],
+    "infra_step": true,
+    "name": "glob pw_env_setup/.env_setup*",
+    "~followup_annotations": [
+      "@@@STEP_LOG_END@glob@@@"
+    ]
+  },
+  {
     "name": "$result"
   }
 ]
\ No newline at end of file
diff --git a/recipes/envtest.expected/windows.json b/recipes/envtest.expected/windows.json
index d19a3d2..55e5bcd 100644
--- a/recipes/envtest.expected/windows.json
+++ b/recipes/envtest.expected/windows.json
@@ -283,25 +283,6 @@
       "RECIPE_MODULE[recipe_engine::file]\\resources\\fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
-      "copy",
-      "\"[START_DIR]\\checkout\\pw_env_setup/bootstrap.bat\"\npw --loglevel debug presubmit --step gn_clang_build --repository $PW_ROOT\n",
-      "[START_DIR]\\run.sh"
-    ],
-    "infra_step": true,
-    "name": "write run.sh",
-    "~followup_annotations": [
-      "@@@STEP_LOG_LINE@run.sh@\"[START_DIR]\\checkout\\pw_env_setup/bootstrap.bat\"@@@",
-      "@@@STEP_LOG_LINE@run.sh@pw --loglevel debug presubmit --step gn_clang_build --repository $PW_ROOT@@@",
-      "@@@STEP_LOG_END@run.sh@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "vpython",
-      "-u",
-      "RECIPE_MODULE[recipe_engine::file]\\resources\\fileutil.py",
-      "--json-output",
-      "/path/to/tmp/json",
       "ensure-directory",
       "--mode",
       "0777",
@@ -312,15 +293,67 @@
   },
   {
     "cmd": [
-      "sh",
-      "-x",
-      "[START_DIR]\\run.sh"
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]\\resources\\fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "call [START_DIR]\\\\checkout\\\\pw_env_setup\\\\env_setup.bat\npw --loglevel debug presubmit --step gn_clang_build --repository %PW_ROOT%\n",
+      "[START_DIR]\\run.bat"
+    ],
+    "infra_step": true,
+    "name": "write run.bat",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@run.bat@call [START_DIR]\\\\checkout\\\\pw_env_setup\\\\env_setup.bat@@@",
+      "@@@STEP_LOG_LINE@run.bat@pw --loglevel debug presubmit --step gn_clang_build --repository %PW_ROOT%@@@",
+      "@@@STEP_LOG_END@run.bat@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]\\run.bat"
     ],
     "cwd": "[START_DIR]\\run",
     "env": {
       "PW_CHECKOUT_ROOT": "[START_DIR]\\checkout"
     },
-    "name": "run.sh"
+    "name": "run.bat"
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]\\resources\\fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "glob",
+      "[START_DIR]\\checkout\\pw_env_setup",
+      ".env_setup*"
+    ],
+    "infra_step": true,
+    "name": "glob pw_env_setup/.env_setup*",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@glob@[START_DIR]\\checkout\\pw_env_setup\\.env_setup.bat@@@",
+      "@@@STEP_LOG_END@glob@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]\\resources\\fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[START_DIR]\\checkout\\pw_env_setup\\.env_setup.bat",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "read .env_setup.bat",
+    "~followup_annotations": [
+      "@@@STEP_LOG_END@.env_setup.bat@@@"
+    ]
   },
   {
     "name": "$result"
diff --git a/recipes/envtest.py b/recipes/envtest.py
index 067dec8..929c959 100644
--- a/recipes/envtest.py
+++ b/recipes/envtest.py
@@ -13,7 +13,7 @@
 # the License.
 """Recipe for testing Pigweed using developer env setup scripts."""
 
-import os
+import re
 
 from recipe_engine.recipe_api import Property
 
@@ -38,12 +38,13 @@
         kind=str,
         help=('Path within repository to env setup script to source '
               '(trailing ".sh" will be replaced with ".bat" on Windows)'),
-        default='pw_env_setup/bootstrap.sh',
+        default='pw_env_setup/env_setup.sh',
     ),
 
     'command': Property(
         kind=str,
-        help='Command with which to test.',
+        help=('Command with which to test. Unix-style variables ($ABC) will be '
+              'replaced with Windows-style (%ABC%) automatically on Windows.'),
         default=('pw --loglevel debug presubmit --step gn_clang_build '
                  '--repository $PW_ROOT'),
     ),
@@ -55,22 +56,46 @@
 
   api.checkout(remote)
 
-  if api.platform.is_win and setup_path.endswith('.sh'):
-    setup_path = os.path.splitext(setup_path)[0] + '.bat'
-
-  sh_source = '{dot}"{env_setup}"\n{command}\n'.format(
-      dot='' if api.platform.is_win else '. ',
-      env_setup=api.checkout.root.join(setup_path),
-      command=command,
-  )
-
-  sh_path = api.path['start_dir'].join('run.sh')
-  api.file.write_text('write run.sh', sh_path, sh_source)
-
   run = api.path['start_dir'].join('run')
   api.file.ensure_directory('mkdir run', run)
-  with api.context(cwd=run, env={'PW_CHECKOUT_ROOT': api.checkout.root}):
-    api.step('run.sh', ['sh', '-x', sh_path])
+
+  setup_path = api.checkout.root.join(*re.split(r'[/\\]+', setup_path))
+  env = {}
+
+  if api.platform.is_win and api.path.splitext(setup_path)[1] == '.sh':
+    setup_path = api.path.abs_to_path(
+        api.path.splitext(setup_path)[0] + '.bat')
+
+  env['PW_CHECKOUT_ROOT'] = api.checkout.root
+
+  # Replace '$ABC' with '%ABC%' on Windows.
+  if api.platform.is_win:
+    command = re.sub(r'\$([\w_]+)\b', r'%\1%', command)
+
+  commands = []
+  commands.append('{dot}{env_setup}'.format(
+      dot='call ' if api.platform.is_win else '. ',
+      # Without the replace here we end up with paths that have no
+      # separators. Not sure why, but this fixes it.
+      env_setup=api.path.realpath(setup_path).replace('\\', '\\\\')))
+  commands.append(command)
+
+  sh_source = ''.join(x + '\n' for x in commands)
+  base = 'run.bat' if api.platform.is_win else 'run.sh'
+  sh_path = api.path['start_dir'].join(base)
+  api.file.write_text('write {}'.format(base), sh_path, sh_source)
+
+  with api.step.defer_results():
+    with api.context(cwd=run, env=env):
+      if api.platform.is_win:
+        api.step(base, [sh_path])
+      else:
+        api.step(base, ['sh', '-x', sh_path])
+
+    for path in api.file.glob_paths('glob pw_env_setup/.env_setup*',
+                                    api.checkout.root.join('pw_env_setup'),
+                                    '.env_setup*').get_result():
+      api.file.read_text('read {}'.format(api.path.basename(path)), path)
 
 
 def GenTests(api):  # pylint: disable=invalid-name
@@ -83,6 +108,8 @@
       api.test('windows')
       + api.platform.name('win')
       + api.checkout.ci_test_data()
+      + api.step_data('glob pw_env_setup/.env_setup*',
+                      api.file.glob_paths(['.env_setup.bat']))
   )
 
   yield (