From 83fb97b2d40cfe7d09a7617578a0d6b14d367c20 Mon Sep 17 00:00:00 2001
From: GarboMuffin <muffin@mailbox.org>
Date: Wed, 3 Jan 2024 20:58:43 -0600
Subject: [PATCH] Fix building on Windows or with Python 3.12 (#7)

---
 .gitignore |  2 ++
 README.md  | 12 ++++++--
 build.py   | 83 +++++++++++++++++++++++++++++++-----------------------
 3 files changed, 58 insertions(+), 39 deletions(-)

diff --git a/.gitignore b/.gitignore
index ca5e13595f..58963ca197 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,3 +44,5 @@ python_compressed.js
 /*compiler*.jar
 /local_blockly_compressed_vertical.js
 /chromedriver
+# --flagfiles used on Windows
+/*.config
diff --git a/README.md b/README.md
index 145c78e49b..459fc02018 100644
--- a/README.md
+++ b/README.md
@@ -6,21 +6,27 @@
 
 ## Local development
 
+Requires Node.js (`node`), Python (`python3`), and Java (`java`). It is known to work in these environments but should work in many others:
+
+ - Windows 10, Python 3.12.1 (Microsoft Store), Node.js 20.10.0 (nodejs.org installer), Java 11 (Temurin-11.0.21+9)
+ - macOS 14.2.1, Python 3.11.6 (Apple), Node.js 20.10.0 (installed manually), Java 21 (Temurin-21.0.1+12)
+ - Ubuntu 22.04, Python 3.10.12 (python3 package), Node.js 20.10.0 (installed manually), Java 11 (openjdk-11-jre package)
+
 Install dependencies:
 
 ```sh
 npm ci
 ```
 
-The playground to use for local testing is tests/vertical_playground.html.
+Open tests/vertical_playground.html in a browser for development. You don't need to rebuild compressed versions for most changes. Open tests/vertical_playground_compressed.html instead to test if the compressed versions built properly.
 
-To build, run:
+To re-build compressed versions, run:
 
 ```sh
 npm run prepublish
 ```
 
-requires Python (2 or 3). scratch-gui development server must be restarted to update linked scratch-blocks.
+scratch-gui development server must be restarted to update linked scratch-blocks.
 
 <!--
 #### Scratch Blocks is a library for building creative computing interfaces.
diff --git a/build.py b/build.py
index 3fc56e5fd3..44fc4dacfc 100755
--- a/build.py
+++ b/build.py
@@ -36,7 +36,7 @@
 
 import sys
 
-import errno, glob, json, os, re, subprocess, threading, codecs, functools
+import errno, glob, json, os, re, subprocess, threading, codecs, functools, platform
 
 if sys.version_info[0] == 2:
   import httplib
@@ -116,7 +116,7 @@ def run(self):
   if (!isNodeJS) {
     // Find name of current directory.
     var scripts = document.getElementsByTagName('script');
-    var re = new RegExp('(.+)[\/]blockly_uncompressed(_vertical|_horizontal|)\.js$');
+    var re = new RegExp('(.+)[\\/]blockly_uncompressed(_vertical|_horizontal|)\\.js$');
     for (var i = 0, script; script = scripts[i]; i++) {
       var match = re.exec(script.src);
       if (match) {
@@ -333,9 +333,21 @@ def do_compile_local(self, params, target_filename):
       for group in [[CLOSURE_COMPILER_NPM], dash_args]:
         args.extend(filter(lambda item: item, group))
 
+      # On Windows, the command line is too long, so we save the arguments to a file instead
+      use_flagfile = platform.system() == "Windows"
+      if platform.system() == "Windows":
+        flagfile_name = target_filename + ".config"
+        with open(flagfile_name, "w") as f:
+          # \ needs to be escaped still
+          f.write(" ".join(args[1:]).replace("\\", "\\\\"))
+        args = [CLOSURE_COMPILER_NPM, "--flagfile", flagfile_name]
+
       proc = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
       (stdout, stderr) = proc.communicate()
 
+      if use_flagfile:
+        os.remove(flagfile_name)
+
       # Build the JSON response.
       filesizes = [os.path.getsize(value) for (arg, value) in params if arg == "js_file"]
       return dict(
@@ -439,12 +451,12 @@ def write_output(self, target_filename, remove, json_data):
       # The Closure Compiler preserves these.
       LICENSE = re.compile("""/\\*
 
- [\w ]+
+ [\\w ]+
 
  Copyright \\d+ Google Inc.
  https://developers.google.com/blockly/
 
- Licensed under the Apache License, Version 2.0 \(the "License"\);
+ 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
 
@@ -583,28 +595,17 @@ def exclude_horizontal(item):
 
     print("Using local compiler: %s ...\n" % CLOSURE_COMPILER_NPM)
   except (ImportError, AssertionError):
-    print("Using remote compiler: closure-compiler.appspot.com ...\n")
-
-    try:
-      closure_dir = CLOSURE_DIR
-      closure_root = CLOSURE_ROOT
-      closure_library = CLOSURE_LIBRARY
-      closure_compiler = CLOSURE_COMPILER
-
-      calcdeps = import_path(os.path.join(
-          closure_root, closure_library, "closure", "bin", "calcdeps.py"))
-    except ImportError:
-      if os.path.isdir(os.path.join(os.path.pardir, "closure-library-read-only")):
-        # Dir got renamed when Closure moved from Google Code to GitHub in 2014.
-        print("Error: Closure directory needs to be renamed from"
-              "'closure-library-read-only' to 'closure-library'.\n"
-              "Please rename this directory.")
-      elif os.path.isdir(os.path.join(os.path.pardir, "google-closure-library")):
-        print("Error: Closure directory needs to be renamed from"
-             "'google-closure-library' to 'closure-library'.\n"
-             "Please rename this directory.")
-      else:
-        print("""Error: Closure not found.  Read this:
+    if os.path.isdir(os.path.join(os.path.pardir, "closure-library-read-only")):
+      # Dir got renamed when Closure moved from Google Code to GitHub in 2014.
+      print("Error: Closure directory needs to be renamed from"
+            "'closure-library-read-only' to 'closure-library'.\n"
+            "Please rename this directory.")
+    elif os.path.isdir(os.path.join(os.path.pardir, "google-closure-library")):
+      print("Error: Closure directory needs to be renamed from"
+            "'google-closure-library' to 'closure-library'.\n"
+            "Please rename this directory.")
+    else:
+      print("""Error: Closure not found. Usually this means 'npm ci' failed. Try running it again? More resources:
   developers.google.com/blockly/guides/modify/web/closure""")
       sys.exit(1)
 
@@ -624,13 +625,23 @@ def exclude_horizontal(item):
   # Run all tasks in parallel threads.
   # Uncompressed is limited by processor speed.
   # Compressed is limited by network and server speed.
-  # Vertical:
-  Gen_uncompressed(search_paths_vertical, True, closure_env).start()
-  # Horizontal:
-  Gen_uncompressed(search_paths_horizontal, False, closure_env).start()
-
-  # Compressed forms of vertical and horizontal.
-  Gen_compressed(search_paths_vertical, search_paths_horizontal, closure_env).start()
-
-  # This is run locally in a separate thread.
-  # Gen_langfiles().start()
+  threads = [
+    # Vertical:
+    Gen_uncompressed(search_paths_vertical, True, closure_env),
+    # Horizontal:
+    Gen_uncompressed(search_paths_horizontal, False, closure_env),
+    # Compressed forms of vertical and horizontal.
+    Gen_compressed(search_paths_vertical, search_paths_horizontal, closure_env),
+
+    # This is run locally in a separate thread.
+    # Gen_langfiles()
+  ]
+
+  for thread in threads:
+    thread.start()
+
+  # Need to wait for all threads to finish before the main process ends as in Python 3.12,
+  # once the main interpreter is being shutdown, trying to spawn more child threads will
+  # raise "RuntimeError: can't create new thread at interpreter shutdown"
+  for thread in threads:
+    thread.join()