[engine] Add ctx.io.tempdir()

Change-Id: I87d46e15e3887569c65767899ea9c60cbb0fc8c4
Reviewed-on: https://fuchsia-review.googlesource.com/c/shac-project/shac/+/862002
Commit-Queue: Auto-Submit <auto-submit@fuchsia-infra.iam.gserviceaccount.com>
Fuchsia-Auto-Submit: Marc-Antoine Ruel <maruel@google.com>
Reviewed-by: Oliver Newman <olivernewman@google.com>
diff --git a/doc/stdlib.md b/doc/stdlib.md
index e03d92f..c955fac 100644
--- a/doc/stdlib.md
+++ b/doc/stdlib.md
@@ -133,6 +133,7 @@
 Fields:
 
 - read_file
+- tempdir
 
 ## ctx.io.read_file
 
@@ -160,6 +161,11 @@
 
 Content of the file as bytes.
 
+## ctx.io.tempdir
+
+Returns a new temporary directory.
+
+
 ## ctx.os
 
 ctx.os is the object that exposes the API to interact with the operating
diff --git a/doc/stdlib.star b/doc/stdlib.star
index 2fa837f..8f7c821 100644
--- a/doc/stdlib.star
+++ b/doc/stdlib.star
@@ -153,6 +153,11 @@
   pass
 
 
+def _ctx_io_tempdir():
+  """Returns a new temporary directory."""
+  pass
+
+
 def _ctx_os_exec(cmd, cwd = None, env = None, allow_network = False, raise_on_failure = True):
   """Runs a command as a subprocess.
 
@@ -329,6 +334,7 @@
   # ctx.io is the object that exposes the API to interact with the file system.
   io = struct(
     read_file = _ctx_io_read_file,
+    tempdir = _ctx_io_tempdir,
   ),
   # ctx.os is the object that exposes the API to interact with the operating
   # system.
diff --git a/internal/engine/run.go b/internal/engine/run.go
index 2110bd0..c2922fd 100644
--- a/internal/engine/run.go
+++ b/internal/engine/run.go
@@ -434,6 +434,9 @@
 	if err != nil {
 		return "", err
 	}
+	if i >= 1000000 {
+		return "", errors.New("too many temporary directories requested")
+	}
 	p := filepath.Join(s.tmpdir, strconv.Itoa(i))
 	if err = os.Mkdir(p, 0o700); err != nil {
 		return "", err
diff --git a/internal/engine/run_test.go b/internal/engine/run_test.go
index 3104ece..7aba9a0 100644
--- a/internal/engine/run_test.go
+++ b/internal/engine/run_test.go
@@ -1212,16 +1212,22 @@
 		want string
 	}{
 		{
-			name: "ctx-io-read_file-size.star",
-			want: "[//ctx-io-read_file-size.star:16] {\n  \"key\":\n",
+			"ctx-io-read_file-size.star",
+			"[//ctx-io-read_file-size.star:16] {\n  \"key\":\n",
 		},
 		{
-			name: "ctx-io-read_file.star",
-			want: "[//ctx-io-read_file.star:17] {\"key\": \"value\"}\n",
+			"ctx-io-read_file.star",
+			"[//ctx-io-read_file.star:17] {\"key\": \"value\"}\n",
 		},
 		{
-			name: "ctx-os-exec-abspath.star",
-			want: func() string {
+			"ctx-io-tempdir.star",
+			"[//ctx-io-tempdir.star:16] /0/0\n" +
+				"[//ctx-io-tempdir.star:17] /0/1\n" +
+				"[//ctx-io-tempdir.star:18] /0/2\n",
+		},
+		{
+			"ctx-os-exec-abspath.star",
+			func() string {
 				// TODO(maruel): Decide if we want to do CRLF translation automatically.
 				if runtime.GOOS == "windows" {
 					return "[//ctx-os-exec-abspath.star:17] Hello, world\r\n\n"
@@ -1230,8 +1236,8 @@
 			}(),
 		},
 		{
-			name: "ctx-os-exec-env.star",
-			want: func() string {
+			"ctx-os-exec-env.star",
+			func() string {
 				// TODO(maruel): Decide if we want to do CRLF translation automatically.
 				if runtime.GOOS == "windows" {
 					return "[//ctx-os-exec-env.star:24] FOO=foo-value\r\nBAR=bar-value\n"
@@ -1240,54 +1246,54 @@
 			}(),
 		},
 		{
-			name: "ctx-os-exec-success.star",
-			want: "[//ctx-os-exec-success.star:21] retcode: 0\n" +
+			"ctx-os-exec-success.star",
+			"[//ctx-os-exec-success.star:21] retcode: 0\n" +
 				"[//ctx-os-exec-success.star:22] stdout: hello from stdout\n" +
 				"[//ctx-os-exec-success.star:23] stderr: hello from stderr\n",
 		},
 		{
-			name: "ctx-re-allmatches.star",
-			want: "[//ctx-re-allmatches.star:17] ()\n" +
+			"ctx-re-allmatches.star",
+			"[//ctx-re-allmatches.star:17] ()\n" +
 				"[//ctx-re-allmatches.star:19] (match(groups = (\"TODO(foo)\",), offset = 4), match(groups = (\"TODO(bar)\",), offset = 14))\n" +
 				"[//ctx-re-allmatches.star:21] (match(groups = (\"anc\", \"n\", \"c\"), offset = 0),)\n",
 		},
 		{
-			name: "ctx-re-match.star",
-			want: "[//ctx-re-match.star:17] None\n" +
+			"ctx-re-match.star",
+			"[//ctx-re-match.star:17] None\n" +
 				"[//ctx-re-match.star:19] match(groups = (\"TODO(foo)\",), offset = 4)\n" +
 				"[//ctx-re-match.star:21] match(groups = (\"anc\", \"n\", \"c\"), offset = 0)\n" +
 				"[//ctx-re-match.star:23] match(groups = (\"a\", None), offset = 0)\n",
 		},
 		{
-			name: "dir-ctx.star",
-			want: "[//dir-ctx.star:16] [\"emit\", \"io\", \"os\", \"re\", \"scm\"]\n",
+			"dir-ctx.star",
+			"[//dir-ctx.star:16] [\"emit\", \"io\", \"os\", \"re\", \"scm\"]\n",
 		},
 		{
-			name: "dir-shac.star",
-			want: "[//dir-shac.star:15] [\"check\", \"commit_hash\", \"register_check\", \"version\"]\n",
+			"dir-shac.star",
+			"[//dir-shac.star:15] [\"check\", \"commit_hash\", \"register_check\", \"version\"]\n",
 		},
 		{
-			name: "print-shac-version.star",
-			want: "[//print-shac-version.star:15] " + v + "\n",
+			"print-shac-version.star",
+			"[//print-shac-version.star:15] " + v + "\n",
 		},
 		{
-			name: "shac-check.star",
-			want: "[//shac-check.star:19] str(check): <check hello_world>\n" +
+			"shac-check.star",
+			"[//shac-check.star:19] str(check): <check hello_world>\n" +
 				"[//shac-check.star:20] type(check): shac.check\n" +
 				"[//shac-check.star:21] bool(check): True\n" +
 				"[//shac-check.star:26] hashed: set([<check hello_world>])\n",
 		},
 		{
-			name: "shac-register_check-object.star",
-			want: "[//shac-register_check-object.star:16] running from a check object\n",
+			"shac-register_check-object.star",
+			"[//shac-register_check-object.star:16] running from a check object\n",
 		},
 		{
-			name: "shac-register_check.star",
-			want: "[//shac-register_check.star:16] running\n",
+			"shac-register_check.star",
+			"[//shac-register_check.star:16] running\n",
 		},
 		{
-			name: "true.star",
-			want: "[//true.star:15] True\n",
+			"true.star",
+			"[//true.star:15] True\n",
 		},
 	}
 	want := make([]string, len(data))
diff --git a/internal/engine/runtime_ctx.go b/internal/engine/runtime_ctx.go
index 19a30e0..8a55905 100644
--- a/internal/engine/runtime_ctx.go
+++ b/internal/engine/runtime_ctx.go
@@ -32,6 +32,7 @@
 		}),
 		"io": toValue("ctx.io", starlark.StringDict{
 			"read_file": newBuiltin("ctx.io.read_file", ctxIoReadFile),
+			"tempdir":   newBuiltin("ctx.io.tempdir", ctxIoTempdir),
 		}),
 		"os": toValue("ctx.os", starlark.StringDict{
 			"exec": newBuiltin("ctx.os.exec", ctxOsExec),
diff --git a/internal/engine/runtime_ctx_io.go b/internal/engine/runtime_ctx_io.go
index faa4cb1..94001d5 100644
--- a/internal/engine/runtime_ctx_io.go
+++ b/internal/engine/runtime_ctx_io.go
@@ -63,6 +63,17 @@
 	return starlark.Bytes(b), nil
 }
 
+// ctxIoTempdir implements native function ctx.io.tempdir().
+//
+// Make sure to update //doc/stdlib.star whenever this function is modified.
+func ctxIoTempdir(ctx context.Context, s *shacState, name string, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
+	if err := starlark.UnpackArgs(name, args, kwargs); err != nil {
+		return nil, err
+	}
+	t, err := s.newTempDir()
+	return starlark.String(t), err
+}
+
 // Support functions.
 
 // readFileImpl is similar to os.ReadFile() albeit it limits the amount of data
diff --git a/internal/engine/testdata/print/ctx-io-tempdir.star b/internal/engine/testdata/print/ctx-io-tempdir.star
new file mode 100644
index 0000000..6041047
--- /dev/null
+++ b/internal/engine/testdata/print/ctx-io-tempdir.star
@@ -0,0 +1,20 @@
+# Copyright 2023 The Shac Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+def cb(ctx):
+  print(ctx.io.tempdir()[-4:])
+  print(ctx.io.tempdir()[-4:])
+  print(ctx.io.tempdir()[-4:])
+
+shac.register_check(cb)