Support serving demos from a dedicated directory, and add script for populating that directory from a repo.

Change-Id: I9d961a6e7f05e1af1ec8239370ac41b2bca73131
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/285659
Commit-Queue: Weston Tracey <westont@google.com>
Reviewed-by: Kevin Lubick <kjlubick@google.com>
diff --git a/demos/.gitignore b/demos/.gitignore
new file mode 100644
index 0000000..f5a6742
--- /dev/null
+++ b/demos/.gitignore
@@ -0,0 +1,2 @@
+fetched_demos/
+build/
diff --git a/demos/Dockerfile b/demos/Dockerfile
index 820c7d6..1beed27 100644
--- a/demos/Dockerfile
+++ b/demos/Dockerfile
@@ -5,5 +5,7 @@
 USER skia
 
 ENTRYPOINT ["/usr/local/bin/demoserver"]
-CMD ["--logtostderr", "--port=:8000", "--resources_dir=/usr/local/share/demoserver/"]
+CMD ["--logtostderr", "--port=:8000", \
+        "--resources_dir=/usr/local/share/demoserver/dist", \
+        "--demos_dir=/usr/local/share/demoserver/demos"]
 
diff --git a/demos/Makefile b/demos/Makefile
index 5841aa1..7da3a9e 100644
--- a/demos/Makefile
+++ b/demos/Makefile
@@ -4,12 +4,38 @@
 serve:
 	npx webpack-dev-server --watch-poll --mode-development
 
-build_resources:
+fetch_demos:
+	go run ./go/load-demo-assets/main.go \
+		--alsologtostderr \
+		--demos_dir "demos/internal" \
+		--out_dir "fetched_demos" \
+		--repo_url "https://skia.googlesource.com/infra-internal"
+
+wasm_libs:
+	rm -rf build
+	mkdir -p build/canvaskit build/pathkit
+	# Extract them from the most recently built ones.
+	# Run as current user so they have non-root ownership.
+	docker run -u $$(id -u ${USER}):$$(id -g ${USER}) --rm \
+		--volume `pwd`/build:/OUT gcr.io/skia-public/skia-wasm-release:prod \
+		sh -c "cp -r /tmp/* /OUT/"
+	echo "export const SKIA_VERSION = '`cat build/VERSION`';" > build/version.js
+
+build_resources: wasm_libs
 	npx webpack --mode=production
 
 run: build_resources core
-	demoserver --alsologtostderr
+	demoserver \
+	--alsologtostderr \
+	--port=:8000 \
+	--resources_dir="dist" \
+	--demos_dir="fetched_demos"
 
-release: build_resources
+
+release: build_resources fetch_demos
 	CGO_ENABLED=0 GOOS=linux go install -a ./go/demoserver
 	./build_release
+
+local_image: build_resources fetch_demos
+	CGO_ENABLED=0 GOOS=linux go install -a ./go/demoserver
+	SKIP_UPLOAD=1 ./build_release
diff --git a/demos/build_release b/demos/build_release
index 71dae5e..af86384 100755
--- a/demos/build_release
+++ b/demos/build_release
@@ -8,10 +8,14 @@
 {
 INSTALL="install -D --verbose --backup=none"
 INSTALL_DIR="install -d --verbose --backup=none"
-${INSTALL} --mode=644 -T Dockerfile               ${ROOT}/Dockerfile
-${INSTALL} --mode=755 -T ${GOPATH}/bin/${APPNAME} ${ROOT}/usr/local/bin/${APPNAME}
-${INSTALL_DIR} --mode=755                         ${ROOT}/usr/local/share/${APPNAME}/
-${INSTALL} --mode=644    ./dist/*                 ${ROOT}/usr/local/share/${APPNAME}/
+${INSTALL}     --mode=644 -T Dockerfile               ${ROOT}/Dockerfile
+${INSTALL}     --mode=755 -T ${GOPATH}/bin/${APPNAME} ${ROOT}/usr/local/bin/${APPNAME}
+# Resources for the main page.
+${INSTALL_DIR} --mode=755                             ${ROOT}/usr/local/share/${APPNAME}/
+${INSTALL}     --mode=755 ./dist/*                    ${ROOT}/usr/local/share/${APPNAME}/
+# Demo pages. 'install' isn't recursive.
+cp -r                     ./fetched_demos             ${ROOT}/usr/local/share/${APPNAME}/demos
+chmod -R       a=rX,u=rwX                             ${ROOT}/usr/local/share/${APPNAME}/demos
 }
 
 source ../bash/docker_build.sh
diff --git a/demos/go/demoserver/main.go b/demos/go/demoserver/main.go
index 8b9b125..c1e398c 100644
--- a/demos/go/demoserver/main.go
+++ b/demos/go/demoserver/main.go
@@ -1,6 +1,6 @@
 package main
 
-// The webserver for demos.skia.org. It serves the main page to navigate among JS demos.
+// The webserver for demos.skia.org. It serves a main page and a set of js+html+css demos.
 
 import (
 	"flag"
@@ -15,6 +15,7 @@
 
 var (
 	port         = flag.String("port", ":8000", "HTTP service address (e.g., ':8000')")
+	demosDir     = flag.String("demos_dir", "./demos/public", "The directory to find named subdirectories for each demo. If blank ./demos/public")
 	resourcesDir = flag.String("resources_dir", "./dist", "The directory to find templates, JS, and CSS files. If blank ./dist will be used.")
 )
 
@@ -22,7 +23,12 @@
 	common.InitWithMust(
 		"demos",
 	)
+
 	r := mux.NewRouter()
+	r.PathPrefix("/demo/").Handler(http.StripPrefix("/demo", http.FileServer(http.Dir(*demosDir))))
+	// PathPrefix above needs a slash to make FileServer relative paths work.
+	// For cleanliness, make sure users get to the directory listing even without the slash.
+	r.Handle("/demo", http.RedirectHandler("/demo/", 301))
 	r.PathPrefix("/dist/").Handler(http.StripPrefix("/dist/", http.FileServer(http.Dir(*resourcesDir))))
 	r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
 		http.ServeFile(w, r, filepath.Join(*resourcesDir, "main.html"))
diff --git a/demos/go/load-demo-assets/main.go b/demos/go/load-demo-assets/main.go
new file mode 100644
index 0000000..c5037d1
--- /dev/null
+++ b/demos/go/load-demo-assets/main.go
@@ -0,0 +1,45 @@
+package main
+
+// Loads all checked in demos from infra-internal to be bundled into the demoserver image for
+// release.
+
+import (
+	"context"
+	"flag"
+	"path/filepath"
+
+	"go.skia.org/infra/go/auth"
+	"go.skia.org/infra/go/gitiles"
+	"go.skia.org/infra/go/httputils"
+
+	"go.skia.org/infra/go/sklog"
+)
+
+var (
+	demosRepo     = flag.String("repo_url", "https://skia.googlesource.com/infra-internal", "The repo from where to fetch the demos. Defaults to https://skia.googlesource.com/infra-internal")
+	demosRepoPath = flag.String("demos_dir", "scripts", "The top level directory in the repo that holds the demos.")
+	outDir        = flag.String("out_dir", "./out", "Where the demos from demos_dir should be downloaded to, directories will be created as needed.")
+)
+
+func main() {
+	flag.Parse()
+	ts, err := auth.NewDefaultTokenSource(true, auth.SCOPE_USERINFO_EMAIL, auth.SCOPE_GERRIT)
+	if err != nil {
+		sklog.Fatal(err)
+	}
+	client := httputils.DefaultClientConfig().WithTokenSource(ts).Client()
+	repo := gitiles.NewRepo(*demosRepo, client)
+	files, err := repo.ListFilesRecursive(context.Background(), *demosRepoPath)
+	if err != nil {
+		sklog.Fatal(err)
+	}
+	sklog.Infof("Downloading files: %v", files)
+
+	for _, f := range files {
+		err := repo.DownloadFile(context.Background(), filepath.Join(*demosRepoPath, f), filepath.Join(*outDir, f))
+		if err != nil {
+			sklog.Fatal(err)
+		}
+	}
+
+}
diff --git a/demos/package-lock.json b/demos/package-lock.json
index 4420325..8901e44 100644
--- a/demos/package-lock.json
+++ b/demos/package-lock.json
@@ -1,5 +1,5 @@
 {
-  "name": "ctfe",
+  "name": "demos",
   "version": "0.1.0",
   "lockfileVersion": 1,
   "requires": true,
@@ -2495,6 +2495,108 @@
       "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
       "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40="
     },
+    "copy-webpack-plugin": {
+      "version": "4.6.0",
+      "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-4.6.0.tgz",
+      "integrity": "sha512-Y+SQCF+0NoWQryez2zXn5J5knmr9z/9qSQt7fbL78u83rxmigOy8X5+BFn8CFSuX+nKT8gpYwJX68ekqtQt6ZA==",
+      "dev": true,
+      "requires": {
+        "cacache": "^10.0.4",
+        "find-cache-dir": "^1.0.0",
+        "globby": "^7.1.1",
+        "is-glob": "^4.0.0",
+        "loader-utils": "^1.1.0",
+        "minimatch": "^3.0.4",
+        "p-limit": "^1.0.0",
+        "serialize-javascript": "^1.4.0"
+      },
+      "dependencies": {
+        "cacache": {
+          "version": "10.0.4",
+          "resolved": "https://registry.npmjs.org/cacache/-/cacache-10.0.4.tgz",
+          "integrity": "sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA==",
+          "dev": true,
+          "requires": {
+            "bluebird": "^3.5.1",
+            "chownr": "^1.0.1",
+            "glob": "^7.1.2",
+            "graceful-fs": "^4.1.11",
+            "lru-cache": "^4.1.1",
+            "mississippi": "^2.0.0",
+            "mkdirp": "^0.5.1",
+            "move-concurrently": "^1.0.1",
+            "promise-inflight": "^1.0.1",
+            "rimraf": "^2.6.2",
+            "ssri": "^5.2.4",
+            "unique-filename": "^1.1.0",
+            "y18n": "^4.0.0"
+          }
+        },
+        "globby": {
+          "version": "7.1.1",
+          "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz",
+          "integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=",
+          "dev": true,
+          "requires": {
+            "array-union": "^1.0.1",
+            "dir-glob": "^2.0.0",
+            "glob": "^7.1.2",
+            "ignore": "^3.3.5",
+            "pify": "^3.0.0",
+            "slash": "^1.0.0"
+          }
+        },
+        "mississippi": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-2.0.0.tgz",
+          "integrity": "sha512-zHo8v+otD1J10j/tC+VNoGK9keCuByhKovAvdn74dmxJl9+mWHnx6EMsDN4lgRoMI/eYo2nchAxniIbUPb5onw==",
+          "dev": true,
+          "requires": {
+            "concat-stream": "^1.5.0",
+            "duplexify": "^3.4.2",
+            "end-of-stream": "^1.1.0",
+            "flush-write-stream": "^1.0.0",
+            "from2": "^2.1.0",
+            "parallel-transform": "^1.1.0",
+            "pump": "^2.0.1",
+            "pumpify": "^1.3.3",
+            "stream-each": "^1.1.0",
+            "through2": "^2.0.0"
+          }
+        },
+        "pump": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
+          "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
+          "dev": true,
+          "requires": {
+            "end-of-stream": "^1.1.0",
+            "once": "^1.3.1"
+          }
+        },
+        "serialize-javascript": {
+          "version": "1.9.1",
+          "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.9.1.tgz",
+          "integrity": "sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A==",
+          "dev": true
+        },
+        "ssri": {
+          "version": "5.3.0",
+          "resolved": "https://registry.npmjs.org/ssri/-/ssri-5.3.0.tgz",
+          "integrity": "sha512-XRSIPqLij52MtgoQavH/x/dU1qVKtWUAAZeOHsR9c2Ddi4XerFy3mc1alf+dLJKl9EUIm/Ht+EowFkTUOA6GAQ==",
+          "dev": true,
+          "requires": {
+            "safe-buffer": "^5.1.1"
+          }
+        },
+        "y18n": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
+          "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
+          "dev": true
+        }
+      }
+    },
     "core-js": {
       "version": "2.6.11",
       "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz",
@@ -3136,6 +3238,26 @@
         "randombytes": "^2.0.0"
       }
     },
+    "dir-glob": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz",
+      "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==",
+      "dev": true,
+      "requires": {
+        "path-type": "^3.0.0"
+      },
+      "dependencies": {
+        "path-type": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
+          "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==",
+          "dev": true,
+          "requires": {
+            "pify": "^3.0.0"
+          }
+        }
+      }
+    },
     "dns-equal": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz",
@@ -5629,6 +5751,12 @@
       "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz",
       "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE="
     },
+    "ignore": {
+      "version": "3.3.10",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz",
+      "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==",
+      "dev": true
+    },
     "import-cwd": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
diff --git a/demos/package.json b/demos/package.json
index 33d3400..227084d 100644
--- a/demos/package.json
+++ b/demos/package.json
@@ -13,6 +13,7 @@
     "autoprefixer": "^9.7.4",
     "bower": "^1.8.8",
     "chai": "~4.2.0",
+    "copy-webpack-plugin": "^4.6.0",
     "cssmin": "~0.4.2",
     "fetch-mock": "~7.3.9",
     "html-minifier": "~3.0.0",
diff --git a/demos/webpack.config.js b/demos/webpack.config.js
index 9ccae9e..80e4fad 100644
--- a/demos/webpack.config.js
+++ b/demos/webpack.config.js
@@ -1,5 +1,6 @@
 const commonBuilder = require('pulito');
-const {resolve} = require('path');
+const CopyWebpackPlugin = require('copy-webpack-plugin');
+const { resolve } = require('path');
 
 module.exports = (env, argv) => {
   const config = commonBuilder(env, argv, __dirname);
@@ -7,6 +8,16 @@
   config.resolve = config.resolve || {};
   // https://github.com/webpack/node-libs-browser/issues/26#issuecomment-267954095
   config.resolve.modules = [resolve(__dirname, 'node_modules'), 'node_modules'];
+  config.plugins.push(
+    new CopyWebpackPlugin([
+      { from: resolve(__dirname, 'build/canvaskit/canvaskit.wasm') },
+      { from: resolve(__dirname, 'build/canvaskit/canvaskit.js') },
+      { from: resolve(__dirname, 'node_modules/@webcomponents/custom-elements/custom-elements.min.js') },
+    ]),
+  );
+  config.node = {
+    fs: 'empty',
+  };
 
   return config;
-}
+};
diff --git a/go/util/util.go b/go/util/util.go
index a2c6310..7ec3d55 100644
--- a/go/util/util.go
+++ b/go/util/util.go
@@ -622,7 +622,11 @@
 // temporary intermediate file for more atomicity in case a long-running write
 // gets interrupted.
 func WithWriteFile(file string, writeFn func(io.Writer) error) error {
-	f, err := ioutil.TempFile(path.Dir(file), path.Base(file))
+	dir := path.Dir(file)
+	if err := os.MkdirAll(dir, 700); err != nil {
+		return fmt.Errorf("Failed to MkdirAll(%s, 700): %v", dir, err)
+	}
+	f, err := ioutil.TempFile(dir, path.Base(file))
 	if err != nil {
 		return fmt.Errorf("Failed to create temporary file for WithWriteFile: %s", err)
 	}