merged
authorwenzelm
Wed, 30 Jun 2021 21:35:30 +0200
changeset 73908 506734c805ac
parent 73887 9b981f5612d0 (current diff)
parent 73907 8cc891183484 (diff)
child 73909 1d0d9772fff0
merged
--- a/Admin/components/components.sha1	Wed Jun 30 09:11:31 2021 +0200
+++ b/Admin/components/components.sha1	Wed Jun 30 21:35:30 2021 +0200
@@ -121,6 +121,7 @@
 b166b4bd583b6442a5d75eab06f7adbb66919d6d  isabelle_fonts-20210319.tar.gz
 9467ad54a9ac10a6e7e8db5458d8d2a5516eba96  isabelle_fonts-20210321.tar.gz
 1f7a0b9829ecac6552b21e995ad0f0ac168634f3  isabelle_fonts-20210322.tar.gz
+916adccd2f40c55116b68b92ce1eccb24d4dd9a2  isabelle_setup-20210630.tar.gz
 0b2206f914336dec4923dd0479d8cee4b904f544  jdk-11+28.tar.gz
 e12574d838ed55ef2845acf1152329572ab0cc56  jdk-11.0.10+9.tar.gz
 3e05213cad47dbef52804fe329395db9b4e57f39  jdk-11.0.2+9.tar.gz
@@ -346,6 +347,7 @@
 a0622fe75c3482ba7dc3ce74d58583b648a1ff0d  scala-2.13.4-1.tar.gz
 ec53cce3c5edda1145ec5d13924a5f9418995c15  scala-2.13.4.tar.gz
 f51981baf34c020ad103b262f81796c37abcaa4a  scala-2.13.5.tar.gz
+0a7cab09dec357dab7819273f2542ff1c3ea0968  scala-2.13.6.tar.gz
 b447017e81600cc5e30dd61b5d4962f6da01aa80  scala-2.8.1.final.tar.gz
 5659440f6b86db29f0c9c0de7249b7e24a647126  scala-2.9.2.tar.gz
 abe7a3b50da529d557a478e9f631a22429418a67  smbc-0.4.1.tar.gz
--- a/Admin/components/main	Wed Jun 30 09:11:31 2021 +0200
+++ b/Admin/components/main	Wed Jun 30 21:35:30 2021 +0200
@@ -8,6 +8,7 @@
 flatlaf-1.2
 idea-icons-20210508
 isabelle_fonts-20210322
+isabelle_setup-20210630
 jdk-15.0.2+7
 jedit_build-20210510-1
 jfreechart-1.5.1
@@ -17,7 +18,7 @@
 opam-2.0.7
 polyml-5.8.2
 postgresql-42.2.18
-scala-2.13.5
+scala-2.13.6
 smbc-0.4.1
 spass-3.8ds-2
 sqlite-jdbc-3.34.0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Admin/lib/Tools/build_setup	Wed Jun 30 21:35:30 2021 +0200
@@ -0,0 +1,83 @@
+#!/usr/bin/env bash
+#
+# Author: Makarius
+#
+# DESCRIPTION: build component for Isabelle/Java setup tool
+
+## sources
+
+declare -a SOURCES=(
+  "Environment.java"
+  "Setup.java"
+)
+
+
+## usage
+
+PRG=$(basename "$0")
+
+function usage()
+{
+  echo
+  echo "Usage: isabelle $PRG [OPTIONS] COMPONENT_DIR"
+  echo
+  echo "  Build component for Isabelle/Java setup tool."
+  echo
+  exit 1
+}
+
+function fail()
+{
+  echo "$1" >&2
+  exit 2
+}
+
+
+## process command line
+
+[ "$#" -ge 1 ] && { COMPONENT_DIR="$1"; shift; }
+[ "$#" -ne 0 -o -z "$COMPONENT_DIR" ] && usage
+
+
+
+## main
+
+[ -d "$COMPONENT_DIR" ] && fail "Directory already exists: \"$COMPONENT_DIR\""
+
+
+# build jar
+
+TARGET_DIR="$COMPONENT_DIR/lib"
+mkdir -p "$TARGET_DIR/isabelle/setup"
+
+declare -a ARGS=("-Xlint:unchecked")
+for SRC in "${SOURCES[@]}"
+do
+  ARGS["${#ARGS[@]}"]="$(platform_path "$ISABELLE_HOME/src/Tools/Setup/src/isabelle/setup/$SRC")"
+done
+
+isabelle_jdk javac -d "$TARGET_DIR" "${ARGS[@]}" || \
+  fail "Failed to compile sources"
+
+isabelle_jdk jar -c -f "$(platform_path "$TARGET_DIR/isabelle_setup.jar")" \
+  -e "isabelle.setup.Setup" -C "$TARGET_DIR" isabelle || fail "Failed to produce jar"
+
+rm -rf "$TARGET_DIR/isabelle"
+
+
+# etc/settings
+
+mkdir -p "$COMPONENT_DIR/etc"
+cat > "$COMPONENT_DIR/etc/settings" <<EOF
+# -*- shell-script -*- :mode=shellscript:
+
+ISABELLE_SETUP_JAR="\$COMPONENT/lib/isabelle_setup.jar"
+classpath "\$ISABELLE_SETUP_JAR"
+EOF
+
+
+# README
+
+cat > "$COMPONENT_DIR/README" <<EOF
+Isabelle setup in pure Java, see also \$ISABELLE_HOME/src/Tools/Setup/.
+EOF
--- a/src/Pure/Admin/build_release.scala	Wed Jun 30 09:11:31 2021 +0200
+++ b/src/Pure/Admin/build_release.scala	Wed Jun 30 21:35:30 2021 +0200
@@ -861,7 +861,7 @@
       var rev = ""
 
       val getopts = Getopts("""
-Usage: Admin/build_release [OPTIONS] BASE_DIR
+Usage: Admin/build_release [OPTIONS]
 
   Options are:
     -A REV       corresponding AFP changeset id
--- a/src/Pure/General/file.scala	Wed Jun 30 09:11:31 2021 +0200
+++ b/src/Pure/General/file.scala	Wed Jun 30 21:35:30 2021 +0200
@@ -15,13 +15,11 @@
 import java.nio.file.attribute.BasicFileAttributes
 import java.net.{URL, MalformedURLException}
 import java.util.zip.{GZIPInputStream, GZIPOutputStream}
-import java.util.regex.Pattern
 import java.util.EnumSet
 
 import org.tukaani.xz.{XZInputStream, XZOutputStream}
 
 import scala.collection.mutable
-import scala.util.matching.Regex
 
 
 object File
@@ -31,20 +29,7 @@
   def standard_path(path: Path): String = path.expand.implode
 
   def standard_path(platform_path: String): String =
-    if (Platform.is_windows) {
-      val Platform_Root = new Regex("(?i)" +
-        Pattern.quote(Isabelle_System.cygwin_root()) + """(?:\\+|\z)(.*)""")
-      val Drive = new Regex("""([a-zA-Z]):\\*(.*)""")
-
-      platform_path.replace('/', '\\') match {
-        case Platform_Root(rest) => "/" + rest.replace('\\', '/')
-        case Drive(letter, rest) =>
-          "/cygdrive/" + Word.lowercase(letter) +
-            (if (rest == "") "" else "/" + rest.replace('\\', '/'))
-        case path => path.replace('\\', '/')
-      }
-    }
-    else platform_path
+    isabelle.setup.Environment.standard_path(Isabelle_System.cygwin_root(), platform_path)
 
   def standard_path(file: JFile): String = standard_path(file.getPath)
 
@@ -60,36 +45,8 @@
 
   /* platform path (Windows or Posix) */
 
-  private val Cygdrive = new Regex("/cygdrive/([a-zA-Z])($|/.*)")
-  private val Named_Root = new Regex("//+([^/]*)(.*)")
-
   def platform_path(standard_path: String): String =
-    if (Platform.is_windows) {
-      val result_path = new StringBuilder
-      val rest =
-        standard_path match {
-          case Cygdrive(drive, rest) =>
-            result_path ++= (Word.uppercase(drive) + ":" + JFile.separator)
-            rest
-          case Named_Root(root, rest) =>
-            result_path ++= JFile.separator
-            result_path ++= JFile.separator
-            result_path ++= root
-            rest
-          case path if path.startsWith("/") =>
-            result_path ++= Isabelle_System.cygwin_root()
-            path
-          case path => path
-        }
-      for (p <- space_explode('/', rest) if p != "") {
-        val len = result_path.length
-        if (len > 0 && result_path(len - 1) != JFile.separatorChar)
-          result_path += JFile.separatorChar
-        result_path ++= p
-      }
-      result_path.toString
-    }
-    else standard_path
+    isabelle.setup.Environment.platform_path(Isabelle_System.cygwin_root(), standard_path)
 
   def platform_path(path: Path): String = platform_path(standard_path(path))
   def platform_file(path: Path): JFile = new JFile(platform_path(path))
--- a/src/Pure/General/path.scala	Wed Jun 30 09:11:31 2021 +0200
+++ b/src/Pure/General/path.scala	Wed Jun 30 21:35:30 2021 +0200
@@ -8,6 +8,7 @@
 package isabelle
 
 
+import java.util.{Map => JMap}
 import java.io.{File => JFile}
 
 import scala.util.matching.Regex
@@ -256,7 +257,7 @@
 
   /* expand */
 
-  def expand_env(env: Map[String, String]): Path =
+  def expand_env(env: JMap[String, String]): Path =
   {
     def eval(elem: Path.Elem): List[Path.Elem] =
       elem match {
--- a/src/Pure/General/ssh.scala	Wed Jun 30 09:11:31 2021 +0200
+++ b/src/Pure/General/ssh.scala	Wed Jun 30 21:35:30 2021 +0200
@@ -7,6 +7,7 @@
 package isabelle
 
 
+import java.util.{Map => JMap, HashMap}
 import java.io.{InputStream, OutputStream, ByteArrayOutputStream}
 
 import scala.collection.mutable
@@ -349,10 +350,10 @@
 
     override def close(): Unit = { sftp.disconnect; session.disconnect; on_close() }
 
-    val settings: Map[String, String] =
+    val settings: JMap[String, String] =
     {
       val home = sftp.getHome
-      Map("HOME" -> home, "USER_HOME" -> home)
+      JMap.of("HOME", home, "USER_HOME", home)
     }
     override def expand_path(path: Path): Path = path.expand_env(settings)
     def remote_path(path: Path): String = expand_path(path).implode
--- a/src/Pure/ML/ml_process.scala	Wed Jun 30 09:11:31 2021 +0200
+++ b/src/Pure/ML/ml_process.scala	Wed Jun 30 21:35:30 2021 +0200
@@ -7,6 +7,7 @@
 package isabelle
 
 
+import java.util.{Map => JMap, HashMap}
 import java.io.{File => JFile}
 
 
@@ -22,7 +23,7 @@
     args: List[String] = Nil,
     modes: List[String] = Nil,
     cwd: JFile = null,
-    env: Map[String, String] = Isabelle_System.settings(),
+    env: JMap[String, String] = Isabelle_System.settings(),
     redirect: Boolean = false,
     cleanup: () => Unit = () => (),
     session_base: Option[Sessions.Base] = None): Bash.Process =
@@ -70,11 +71,11 @@
       if (modes.isEmpty) Nil
       else List("Print_Mode.add_modes " + ML_Syntax.print_list(ML_Syntax.print_string_bytes)(modes))
 
+
     // options
     val isabelle_process_options = Isabelle_System.tmp_file("options")
     Isabelle_System.chmod("600", File.path(isabelle_process_options))
     File.write(isabelle_process_options, YXML.string_of_body(options.encode))
-    val env_options = Map("ISABELLE_PROCESS_OPTIONS" -> File.standard_path(isabelle_process_options))
     val eval_options = if (heaps.isEmpty) Nil else List("Options.load_default ()")
 
     // session base
@@ -99,11 +100,6 @@
 
     // ISABELLE_TMP
     val isabelle_tmp = Isabelle_System.tmp_dir("process")
-    val env_tmp =
-      Map("ISABELLE_TMP" -> File.standard_path(isabelle_tmp),
-        "POLYSTATSDIR" -> isabelle_tmp.getAbsolutePath)
-
-    val env_functions = Map("ISABELLE_SCALA_FUNCTIONS" -> Scala.functions.mkString(","))
 
     val ml_runtime_options =
     {
@@ -123,11 +119,17 @@
       (eval_init ::: eval_modes ::: eval_options ::: eval_init_session).flatMap(List("--eval", _)) :::
       use_prelude.flatMap(List("--use", _)) ::: List("--eval", eval_process) ::: args
 
+    val bash_env = new HashMap(env)
+    bash_env.put("ISABELLE_PROCESS_OPTIONS", File.standard_path(isabelle_process_options))
+    bash_env.put("ISABELLE_TMP", File.standard_path(isabelle_tmp))
+    bash_env.put("POLYSTATSDIR", isabelle_tmp.getAbsolutePath)
+    bash_env.put("ISABELLE_SCALA_FUNCTIONS", Scala.functions.mkString(","))
+
     Bash.process(
       options.string("ML_process_policy") + """ "$ML_HOME/poly" -q """ +
         Bash.strings(bash_args),
       cwd = cwd,
-      env = env ++ env_options ++ env_tmp ++ env_functions,
+      env = bash_env,
       redirect = redirect,
       cleanup = () =>
         {
--- a/src/Pure/ROOT.scala	Wed Jun 30 09:11:31 2021 +0200
+++ b/src/Pure/ROOT.scala	Wed Jun 30 21:35:30 2021 +0200
@@ -21,3 +21,4 @@
   val proper_string = Library.proper_string _
   def proper_list[A](list: List[A]): Option[List[A]] = Library.proper_list(list)
 }
+
--- a/src/Pure/System/bash.scala	Wed Jun 30 09:11:31 2021 +0200
+++ b/src/Pure/System/bash.scala	Wed Jun 30 21:35:30 2021 +0200
@@ -7,9 +7,8 @@
 package isabelle
 
 
-import java.io.{File => JFile, BufferedReader, InputStreamReader,
-  BufferedWriter, OutputStreamWriter}
-
+import java.util.{LinkedList, List => JList, Map => JMap}
+import java.io.{BufferedReader, BufferedWriter, InputStreamReader, OutputStreamWriter, File => JFile}
 import scala.annotation.tailrec
 import scala.jdk.OptionConverters._
 
@@ -48,9 +47,24 @@
 
   type Watchdog = (Time, Process => Boolean)
 
+  def process_signal(group_pid: String, signal: String = "0"): Boolean =
+  {
+    val cmd = new LinkedList[String]
+    if (Platform.is_windows) {
+      cmd.add(Isabelle_System.cygwin_root() + "\\bin\\bash.exe")
+    }
+    else {
+      cmd.add("/usr/bin/env")
+      cmd.add("bash")
+    }
+    cmd.add("-c")
+    cmd.add("kill -" + signal + " -" + group_pid)
+    isabelle.setup.Environment.exec_process(cmd, null, null, false).ok
+  }
+
   def process(script: String,
       cwd: JFile = null,
-      env: Map[String, String] = Isabelle_System.settings(),
+      env: JMap[String, String] = Isabelle_System.settings(),
       redirect: Boolean = false,
       cleanup: () => Unit = () => ()): Process =
     new Process(script, cwd, env, redirect, cleanup)
@@ -58,7 +72,7 @@
   class Process private[Bash](
       script: String,
       cwd: JFile,
-      env: Map[String, String],
+      env: JMap[String, String],
       redirect: Boolean,
       cleanup: () => Unit)
   {
@@ -80,10 +94,10 @@
     File.write(script_file, winpid_script)
 
     private val proc =
-      Isabelle_System.process(
-        List(File.platform_path(Path.variable("ISABELLE_BASH_PROCESS")),
+      isabelle.setup.Environment.process_builder(
+        JList.of(File.platform_path(Path.variable("ISABELLE_BASH_PROCESS")),
           File.standard_path(timing_file), "bash", File.standard_path(script_file)),
-        cwd = cwd, env = env, redirect = redirect)
+        cwd, env, redirect).start()
 
 
     // channels
@@ -119,8 +133,8 @@
     {
       count <= 0 ||
       {
-        Isabelle_System.process_signal(group_pid, signal = s)
-        val running = root_process_alive() || Isabelle_System.process_signal(group_pid)
+        process_signal(group_pid, signal = s)
+        val running = root_process_alive() || process_signal(group_pid)
         if (running) {
           Time.seconds(0.1).sleep()
           signal(s, count - 1)
@@ -138,7 +152,7 @@
 
     def interrupt(): Unit = Isabelle_Thread.try_uninterruptible
     {
-      Isabelle_System.process_signal(group_pid, "INT")
+      process_signal(group_pid, "INT")
     }
 
 
--- a/src/Pure/System/cygwin.scala	Wed Jun 30 09:11:31 2021 +0200
+++ b/src/Pure/System/cygwin.scala	Wed Jun 30 21:35:30 2021 +0200
@@ -15,54 +15,4 @@
 
 object Cygwin
 {
-  /* init (e.g. after extraction via 7zip) */
-
-  def init(isabelle_root: String, cygwin_root: String): Unit =
-  {
-    require(Platform.is_windows, "Windows platform expected")
-
-    def exec(cmdline: String*): Unit =
-    {
-      val cwd = new JFile(isabelle_root)
-      val env = sys.env + ("CYGWIN" -> "nodosfilewarning")
-      val proc = Isabelle_System.process(cmdline.toList, cwd = cwd, env = env, redirect = true)
-      val (output, rc) = Isabelle_System.process_output(proc)
-      if (rc != 0) error(output)
-    }
-
-    val uninitialized_file = new JFile(cygwin_root, "isabelle\\uninitialized")
-    val uninitialized = uninitialized_file.isFile && uninitialized_file.delete
-
-    if (uninitialized) {
-      val symlinks =
-      {
-        val path = (new JFile(cygwin_root + "\\isabelle\\symlinks")).toPath
-        Files.readAllLines(path, UTF8.charset).toArray.toList.asInstanceOf[List[String]]
-      }
-      @tailrec def recover_symlinks(list: List[String]): Unit =
-      {
-        list match {
-          case Nil | List("") =>
-          case target :: content :: rest =>
-            link(content, new JFile(isabelle_root, target))
-            recover_symlinks(rest)
-          case _ => error("Unbalanced symlinks list")
-        }
-      }
-      recover_symlinks(symlinks)
-
-      exec(cygwin_root + "\\bin\\dash.exe", "/isabelle/rebaseall")
-      exec(cygwin_root + "\\bin\\bash.exe", "/isabelle/postinstall")
-    }
-  }
-
-  def link(content: String, target: JFile): Unit =
-  {
-    val target_path = target.toPath
-
-    using(Files.newBufferedWriter(target_path, UTF8.charset))(
-      _.write("!<symlink>" + content + "\u0000"))
-
-    Files.setAttribute(target_path, "dos:system", true)
-  }
 }
--- a/src/Pure/System/isabelle_process.scala	Wed Jun 30 09:11:31 2021 +0200
+++ b/src/Pure/System/isabelle_process.scala	Wed Jun 30 21:35:30 2021 +0200
@@ -7,6 +7,7 @@
 package isabelle
 
 
+import java.util.{Map => JMap}
 import java.io.{File => JFile}
 
 
@@ -23,7 +24,7 @@
     eval_main: String = "",
     modes: List[String] = Nil,
     cwd: JFile = null,
-    env: Map[String, String] = Isabelle_System.settings()): Isabelle_Process =
+    env: JMap[String, String] = Isabelle_System.settings()): Isabelle_Process =
   {
     val channel = System_Channel()
     val process =
--- a/src/Pure/System/isabelle_system.scala	Wed Jun 30 09:11:31 2021 +0200
+++ b/src/Pure/System/isabelle_system.scala	Wed Jun 30 21:35:30 2021 +0200
@@ -1,63 +1,41 @@
 /*  Title:      Pure/System/isabelle_system.scala
     Author:     Makarius
 
-Fundamental Isabelle system environment: quasi-static module with
-optional init operation.
+Miscellaneous Isabelle system operations.
 */
 
 package isabelle
 
 
+import java.util.{Map => JMap}
 import java.io.{File => JFile, IOException}
 import java.nio.file.{Path => JPath, Files, SimpleFileVisitor, FileVisitResult,
   StandardCopyOption, FileSystemException}
 import java.nio.file.attribute.BasicFileAttributes
 
-import scala.jdk.CollectionConverters._
-
 
 object Isabelle_System
 {
-  /** bootstrap information **/
+  /* settings */
 
-  def jdk_home(): String =
-  {
-    val java_home = System.getProperty("java.home", "")
-    val home = new JFile(java_home)
-    val parent = home.getParent
-    if (home.getName == "jre" && parent != null &&
-        (new JFile(new JFile(parent, "bin"), "javac")).exists) parent
-    else java_home
-  }
+  def settings(): JMap[String, String] = isabelle.setup.Environment.settings()
 
-  def bootstrap_directory(
-    preference: String, envar: String, property: String, description: String): String =
-  {
-    val value =
-      proper_string(preference) orElse  // explicit argument
-      proper_string(System.getenv(envar)) orElse  // e.g. inherited from running isabelle tool
-      proper_string(System.getProperty(property)) getOrElse  // e.g. via JVM application boot process
-      error("Unknown " + description + " directory")
+  def getenv(name: String, env: JMap[String, String] = settings()): String =
+    Option(env.get(name)).getOrElse("")
 
-    if ((new JFile(value)).isDirectory) value
-    else error("Bad " + description + " directory " + quote(value))
-  }
+  def getenv_strict(name: String, env: JMap[String, String] = settings()): String =
+    proper_string(getenv(name, env)) getOrElse
+      error("Undefined Isabelle environment variable: " + quote(name))
+
+  def cygwin_root(): String = getenv("CYGWIN_ROOT")
 
 
-
-  /** implicit settings environment **/
+  /* services */
 
   abstract class Service
 
-  @volatile private var _settings: Option[Map[String, String]] = None
   @volatile private var _services: Option[List[Class[Service]]] = None
 
-  def settings(): Map[String, String] =
-  {
-    if (_settings.isEmpty) init()  // unsynchronized check
-    _settings.get
-  }
-
   def services(): List[Class[Service]] =
   {
     if (_services.isEmpty) init()  // unsynchronized check
@@ -68,110 +46,32 @@
     for { c1 <- services() if Library.is_subclass(c1, c) }
       yield c1.getDeclaredConstructor().newInstance().asInstanceOf[C]
 
-  def init(isabelle_root: String = "", cygwin_root: String = ""): Unit = synchronized
-  {
-    if (_settings.isEmpty || _services.isEmpty) {
-      val isabelle_root1 =
-        bootstrap_directory(isabelle_root, "ISABELLE_ROOT", "isabelle.root", "Isabelle root")
 
-      val cygwin_root1 =
-        if (Platform.is_windows)
-          bootstrap_directory(cygwin_root, "CYGWIN_ROOT", "cygwin.root", "Cygwin root")
-        else ""
-
-      if (Platform.is_windows) Cygwin.init(isabelle_root1, cygwin_root1)
-
-      def set_cygwin_root(): Unit =
-      {
-        if (Platform.is_windows)
-          _settings = Some(_settings.getOrElse(Map.empty) + ("CYGWIN_ROOT" -> cygwin_root1))
-      }
-
-      set_cygwin_root()
-
-      def default(env: Map[String, String], entry: (String, String)): Map[String, String] =
-        if (env.isDefinedAt(entry._1) || entry._2 == "") env
-        else env + entry
-
-      val env =
-      {
-        val temp_windows =
-        {
-          val temp = if (Platform.is_windows) System.getenv("TEMP") else null
-          if (temp != null && temp.contains('\\')) temp else ""
-        }
-        val user_home = System.getProperty("user.home", "")
-        val isabelle_app = System.getProperty("isabelle.app", "")
-
-        default(
-          default(
-            default(sys.env + ("ISABELLE_JDK_HOME" -> File.standard_path(jdk_home())),
-              "TEMP_WINDOWS" -> temp_windows),
-            "HOME" -> user_home),
-          "ISABELLE_APP" -> "true")
-      }
+  /* init settings + services */
 
-      val settings =
-      {
-        val dump = JFile.createTempFile("settings", null)
-        dump.deleteOnExit
-        try {
-          val cmd1 =
-            if (Platform.is_windows)
-              List(cygwin_root1 + "\\bin\\bash", "-l",
-                File.standard_path(isabelle_root1 + "\\bin\\isabelle"))
-            else
-              List(isabelle_root1 + "/bin/isabelle")
-          val cmd = cmd1 ::: List("getenv", "-d", dump.toString)
-
-          val (output, rc) = process_output(process(cmd, env = env, redirect = true))
-          if (rc != 0) error(output)
-
-          val entries =
-            space_explode('\u0000', File.read(dump)).flatMap(
-              {
-                case Properties.Eq(a, b) => Some(a -> b)
-                case s => if (s.isEmpty || s.startsWith("=")) None else Some(s -> "")
-              }).toMap
-          entries + ("PATH" -> entries("PATH_JVM")) - "PATH_JVM"
-        }
-        finally { dump.delete }
+  def init(isabelle_root: String = "", cygwin_root: String = ""): Unit =
+  {
+    isabelle.setup.Environment.init(isabelle_root, cygwin_root)
+    synchronized {
+      if (_services.isEmpty) {
+        val variable = "ISABELLE_SCALA_SERVICES"
+        val services =
+          for (name <- space_explode(':', getenv_strict(variable)))
+            yield {
+              def err(msg: String): Nothing =
+                error("Bad entry " + quote(name) + " in " + variable + "\n" + msg)
+              try { Class.forName(name).asInstanceOf[Class[Service]] }
+              catch {
+                case _: ClassNotFoundException => err("Class not found")
+                case exn: Throwable => err(Exn.message(exn))
+              }
+            }
+        _services = Some(services)
       }
-      _settings = Some(settings)
-      set_cygwin_root()
-
-      val variable = "ISABELLE_SCALA_SERVICES"
-      val services =
-        for (name <- space_explode(':', settings.getOrElse(variable, getenv_error(variable))))
-        yield {
-          def err(msg: String): Nothing =
-            error("Bad entry " + quote(name) + " in " + variable + "\n" + msg)
-          try { Class.forName(name).asInstanceOf[Class[Service]] }
-          catch {
-            case _: ClassNotFoundException => err("Class not found")
-            case exn: Throwable => err(Exn.message(exn))
-          }
-        }
-      _services = Some(services)
     }
   }
 
 
-  /* getenv -- dynamic process environment */
-
-  private def getenv_error(name: String): Nothing =
-    error("Undefined Isabelle environment variable: " + quote(name))
-
-  def getenv(name: String, env: Map[String, String] = settings()): String =
-    env.getOrElse(name, "")
-
-  def getenv_strict(name: String, env: Map[String, String] = settings()): String =
-    proper_string(getenv(name, env)) getOrElse
-      error("Undefined Isabelle environment variable: " + quote(name))
-
-  def cygwin_root(): String = getenv_strict("CYGWIN_ROOT")
-
-
   /* getetc -- static distribution parameters */
 
   def getetc(name: String, root: Path = Path.ISABELLE_HOME): Option[String] =
@@ -352,12 +252,13 @@
 
     if (force) target.delete
 
+    def cygwin_link(): Unit =
+      isabelle.setup.Environment.cygwin_link(File.standard_path(src), target)
+
     try { Files.createSymbolicLink(target.toPath, src_file.toPath) }
     catch {
-      case _: UnsupportedOperationException if Platform.is_windows =>
-        Cygwin.link(File.standard_path(src), target)
-      case _: FileSystemException if Platform.is_windows =>
-        Cygwin.link(File.standard_path(src), target)
+      case _: UnsupportedOperationException if Platform.is_windows => cygwin_link()
+      case _: FileSystemException if Platform.is_windows => cygwin_link()
     }
   }
 
@@ -458,59 +359,11 @@
 
   /** external processes **/
 
-  /* raw process */
-
-  def process(command_line: List[String],
-    cwd: JFile = null,
-    env: Map[String, String] = settings(),
-    redirect: Boolean = false): Process =
-  {
-    val proc = new ProcessBuilder
-
-    // fragile on Windows:
-    // see https://docs.microsoft.com/en-us/cpp/cpp/main-function-command-line-args?view=msvc-160
-    proc.command(command_line.asJava)
-
-    if (cwd != null) proc.directory(cwd)
-    if (env != null) {
-      proc.environment.clear()
-      for ((x, y) <- env) proc.environment.put(x, y)
-    }
-    proc.redirectErrorStream(redirect)
-    proc.start
-  }
-
-  def process_output(proc: Process): (String, Int) =
-  {
-    proc.getOutputStream.close()
-
-    val output = File.read_stream(proc.getInputStream)
-    val rc =
-      try { proc.waitFor }
-      finally {
-        proc.getInputStream.close()
-        proc.getErrorStream.close()
-        proc.destroy()
-        Exn.Interrupt.dispose()
-      }
-    (output, rc)
-  }
-
-  def process_signal(group_pid: String, signal: String = "0"): Boolean =
-  {
-    val bash =
-      if (Platform.is_windows) List(cygwin_root() + "\\bin\\bash.exe")
-      else List("/usr/bin/env", "bash")
-    val (_, rc) = process_output(process(bash ::: List("-c", "kill -" + signal + " -" + group_pid)))
-    rc == 0
-  }
-
-
   /* GNU bash */
 
   def bash(script: String,
     cwd: JFile = null,
-    env: Map[String, String] = settings(),
+    env: JMap[String, String] = settings(),
     redirect: Boolean = false,
     progress_stdout: String => Unit = (_: String) => (),
     progress_stderr: String => Unit = (_: String) => (),
--- a/src/Pure/System/platform.scala	Wed Jun 30 09:11:31 2021 +0200
+++ b/src/Pure/System/platform.scala	Wed Jun 30 21:35:30 2021 +0200
@@ -11,9 +11,9 @@
 {
   /* platform family */
 
+  val is_windows: Boolean = isabelle.setup.Environment.is_windows()
   val is_linux: Boolean = System.getProperty("os.name", "") == "Linux"
   val is_macos: Boolean = System.getProperty("os.name", "") == "Mac OS X"
-  val is_windows: Boolean = System.getProperty("os.name", "").startsWith("Windows")
   val is_unix: Boolean = is_linux || is_macos
 
   def is_arm: Boolean = cpu_arch.startsWith("arm")
--- a/src/Pure/System/progress.scala	Wed Jun 30 09:11:31 2021 +0200
+++ b/src/Pure/System/progress.scala	Wed Jun 30 21:35:30 2021 +0200
@@ -7,6 +7,7 @@
 package isabelle
 
 
+import java.util.{Map => JMap}
 import java.io.{File => JFile}
 
 
@@ -50,7 +51,7 @@
 
   def bash(script: String,
     cwd: JFile = null,
-    env: Map[String, String] = Isabelle_System.settings(),
+    env: JMap[String, String] = Isabelle_System.settings(),
     redirect: Boolean = false,
     echo: Boolean = false,
     watchdog: Time = Time.zero,
--- a/src/Pure/Tools/build_job.scala	Wed Jun 30 09:11:31 2021 +0200
+++ b/src/Pure/Tools/build_job.scala	Wed Jun 30 21:35:30 2021 +0200
@@ -7,6 +7,8 @@
 package isabelle
 
 
+import java.util.HashMap
+
 import scala.collection.mutable
 
 
@@ -217,9 +219,8 @@
       val base = deps(parent)
       val result_base = deps(session_name)
 
-      val env =
-        Isabelle_System.settings() +
-          ("ISABELLE_ML_DEBUGGER" -> options.bool("ML_debugger").toString)
+      val env = new HashMap(Isabelle_System.settings())
+      env.put("ISABELLE_ML_DEBUGGER", options.bool("ML_debugger").toString)
 
       val is_pure = Sessions.is_pure(session_name)
 
--- a/src/Pure/Tools/scala_project.scala	Wed Jun 30 09:11:31 2021 +0200
+++ b/src/Pure/Tools/scala_project.scala	Wed Jun 30 21:35:30 2021 +0200
@@ -111,12 +111,18 @@
     if (project_dir.is_file || project_dir.is_dir)
       error("Project directory already exists: " + project_dir)
 
-    val src_dir = project_dir + Path.explode("src/main/scala")
     val java_src_dir = project_dir + Path.explode("src/main/java")
     val scala_src_dir = Isabelle_System.make_directory(project_dir + Path.explode("src/main/scala"))
 
     Isabelle_System.copy_dir(Path.explode("~~/src/Tools/jEdit/dist/jEdit"), java_src_dir)
 
+    if (symlinks) {
+      Isabelle_System.symlink(Path.explode("~~/src/Tools/Setup/src/isabelle"), java_src_dir)
+    }
+    else {
+      Isabelle_System.copy_dir(Path.explode("~~/src/Tools/Setup/src"), java_src_dir)
+    }
+
     val files = isabelle_files
     isabelle_scala_files
 
--- a/src/Pure/build-jars	Wed Jun 30 09:11:31 2021 +0200
+++ b/src/Pure/build-jars	Wed Jun 30 21:35:30 2021 +0200
@@ -133,7 +133,6 @@
   src/Pure/System/bash.scala
   src/Pure/System/command_line.scala
   src/Pure/System/components.scala
-  src/Pure/System/cygwin.scala
   src/Pure/System/executable.scala
   src/Pure/System/getopts.scala
   src/Pure/System/isabelle_charset.scala
--- a/src/Tools/Setup/.idea/.name	Wed Jun 30 09:11:31 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-isabelle-setup
\ No newline at end of file
--- a/src/Tools/Setup/.idea/artifacts/Setup_jar.xml	Wed Jun 30 09:11:31 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,8 +0,0 @@
-<component name="ArtifactManager">
-  <artifact type="jar" name="Setup:jar">
-    <output-path>$PROJECT_DIR$/out/artifacts/</output-path>
-    <root id="archive" name="Setup.jar">
-      <element id="module-output" name="Setup" />
-    </root>
-  </artifact>
-</component>
\ No newline at end of file
--- a/src/Tools/Setup/.idea/codeStyles/Project.xml	Wed Jun 30 09:11:31 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-<component name="ProjectCodeStyleConfiguration">
-  <code_scheme name="Project" version="173">
-    <option name="LINE_SEPARATOR" value="&#10;" />
-    <option name="SOFT_MARGINS" value="100" />
-    <codeStyleSettings language="Scala">
-      <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
-    </codeStyleSettings>
-  </code_scheme>
-</component>
\ No newline at end of file
--- a/src/Tools/Setup/.idea/codeStyles/codeStyleConfig.xml	Wed Jun 30 09:11:31 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,5 +0,0 @@
-<component name="ProjectCodeStyleConfiguration">
-  <state>
-    <option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
-  </state>
-</component>
\ No newline at end of file
--- a/src/Tools/Setup/.idea/misc.xml	Wed Jun 30 09:11:31 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK">
-    <output url="file://$PROJECT_DIR$/out" />
-  </component>
-</project>
\ No newline at end of file
--- a/src/Tools/Setup/.idea/modules.xml	Wed Jun 30 09:11:31 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="ProjectModuleManager">
-    <modules>
-      <module fileurl="file://$PROJECT_DIR$/Setup.iml" filepath="$PROJECT_DIR$/Setup.iml" />
-    </modules>
-  </component>
-</project>
\ No newline at end of file
--- a/src/Tools/Setup/.idea/sbt.xml	Wed Jun 30 09:11:31 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="ScalaSbtSettings">
-    <option name="customVMPath" />
-  </component>
-</project>
\ No newline at end of file
--- a/src/Tools/Setup/.idea/vcs.xml	Wed Jun 30 09:11:31 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="VcsDirectoryMappings">
-    <mapping directory="$PROJECT_DIR$/../../.." vcs="hg4idea" />
-  </component>
-</project>
\ No newline at end of file
--- a/src/Tools/Setup/.idea/workspace.xml	Wed Jun 30 09:11:31 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,59 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="ArtifactsWorkspaceSettings">
-    <artifacts-to-build>
-      <artifact name="Setup:jar" />
-    </artifacts-to-build>
-  </component>
-  <component name="AutoImportSettings">
-    <option name="autoReloadType" value="SELECTIVE" />
-  </component>
-  <component name="ChangeListManager">
-    <list default="true" id="a00f79eb-e202-4706-95b3-a972b05b3ddb" name="Default Changelist" comment="" />
-    <option name="SHOW_DIALOG" value="false" />
-    <option name="HIGHLIGHT_CONFLICTS" value="true" />
-    <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
-    <option name="LAST_RESOLUTION" value="IGNORE" />
-  </component>
-  <component name="CodeStyleSettingsInfer">
-    <option name="done" value="true" />
-  </component>
-  <component name="FileTemplateManagerImpl">
-    <option name="RECENT_TEMPLATES">
-      <list>
-        <option value="Class" />
-      </list>
-    </option>
-  </component>
-  <component name="ProjectCodeStyleSettingsMigration">
-    <option name="version" value="1" />
-  </component>
-  <component name="ProjectId" id="1sP6lEsakYWhAQI9WzuHpAcovaN" />
-  <component name="ProjectLevelVcsManager" settingsEditedManually="true" />
-  <component name="ProjectViewState">
-    <option name="hideEmptyMiddlePackages" value="true" />
-    <option name="showLibraryContents" value="true" />
-  </component>
-  <component name="PropertiesComponent">
-    <property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
-    <property name="RunOnceActivity.ShowReadmeOnStart" value="true" />
-    <property name="project.structure.last.edited" value="Artifacts" />
-    <property name="project.structure.proportion" value="0.15" />
-    <property name="project.structure.side.proportion" value="0.18055555" />
-  </component>
-  <component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
-  <component name="TaskManager">
-    <task active="true" id="Default" summary="Default task">
-      <changelist id="a00f79eb-e202-4706-95b3-a972b05b3ddb" name="Default Changelist" comment="" />
-      <created>1620762028428</created>
-      <option name="number" value="Default" />
-      <option name="presentableId" value="Default" />
-      <updated>1620762028428</updated>
-    </task>
-    <servers />
-  </component>
-  <component name="hg4idea.settings">
-    <option name="CHECK_INCOMING_OUTGOING" value="true" />
-    <option name="RECENT_HG_ROOT_PATH" value="$PROJECT_DIR$/../../.." />
-  </component>
-</project>
\ No newline at end of file
--- a/src/Tools/Setup/Setup.iml	Wed Jun 30 09:11:31 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<module type="JAVA_MODULE" version="4">
-  <component name="NewModuleRootManager" inherit-compiler-output="true">
-    <exclude-output />
-    <content url="file://$MODULE_DIR$">
-      <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
-    </content>
-    <orderEntry type="inheritedJdk" />
-    <orderEntry type="sourceFolder" forTests="false" />
-  </component>
-</module>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/Tools/Setup/src/isabelle/setup/Environment.java	Wed Jun 30 21:35:30 2021 +0200
@@ -0,0 +1,336 @@
+/*  Title:      Pure/System/isabelle_env.scala
+    Author:     Makarius
+
+Fundamental Isabelle system environment: quasi-static module with
+optional init operation.
+*/
+
+package isabelle.setup;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.function.BiFunction;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+public class Environment
+{
+    /** Support for Cygwin as POSIX emulation on Windows **/
+
+    public static Boolean is_windows()
+    {
+        return System.getProperty("os.name", "").startsWith("Windows");
+    }
+
+    public static String quote(String s)
+    {
+        return "\"" + s + "\"";
+    }
+
+
+
+    /* system path representations */
+
+    private static String slashes(String s) { return s.replace('\\', '/'); }
+
+    public static String standard_path(String cygwin_root, String platform_path)
+    {
+        if (is_windows()) {
+            String backslashes = platform_path.replace('/', '\\');
+
+            Pattern root_pattern =
+                Pattern.compile("(?i)" + Pattern.quote(cygwin_root) + "(?:\\\\+|\\z)(.*)");
+            Matcher root_matcher = root_pattern.matcher(backslashes);
+
+            Pattern drive_pattern = Pattern.compile("([a-zA-Z]):\\\\*(.*)");
+            Matcher drive_matcher = drive_pattern.matcher(backslashes);
+
+            if (root_matcher.matches()) {
+                String rest = root_matcher.group(1);
+                return "/" + slashes(rest);
+            }
+            else if (drive_matcher.matches()) {
+                String letter = drive_matcher.group(1).toLowerCase(Locale.ROOT);
+                String rest = drive_matcher.group(2);
+                return "/cygdrive/" + letter + (rest.isEmpty() ? "" : "/" + slashes(rest));
+            }
+            else { return slashes(backslashes); }
+        }
+        else { return platform_path; }
+    }
+
+    public static String platform_path(String cygwin_root, String standard_path)
+    {
+        if (is_windows()) {
+            StringBuilder result_path = new StringBuilder();
+
+            Pattern cygdrive_pattern = Pattern.compile("/cygdrive/([a-zA-Z])($|/.*)");
+            Matcher cygdrive_matcher = cygdrive_pattern.matcher(standard_path);
+
+            Pattern named_root_pattern = Pattern.compile("//+([^/]*)(.*)");
+            Matcher named_root_matcher = named_root_pattern.matcher(standard_path);
+
+            String rest;
+            if (cygdrive_matcher.matches()) {
+                String drive = cygdrive_matcher.group(1).toUpperCase(Locale.ROOT);
+                rest = cygdrive_matcher.group(2);
+                result_path.append(drive);
+                result_path.append(':');
+                result_path.append(File.separatorChar);
+            }
+            else if (named_root_matcher.matches()) {
+                String root = named_root_matcher.group(1);
+                rest = named_root_matcher.group(2);
+                result_path.append(File.separatorChar);
+                result_path.append(File.separatorChar);
+                result_path.append(root);
+            }
+            else {
+                if (standard_path.startsWith("/")) { result_path.append(cygwin_root); }
+                rest = standard_path;
+            }
+
+            for (String p : rest.split("/", -1)) {
+                if (!p.isEmpty()) {
+                    int len = result_path.length();
+                    if (len > 0 && result_path.charAt(len - 1) != File.separatorChar) {
+                        result_path.append(File.separatorChar);
+                    }
+                    result_path.append(p);
+                }
+            }
+
+            return result_path.toString();
+        }
+        else { return standard_path; }
+    }
+
+
+    /* raw process */
+
+    public static ProcessBuilder process_builder(
+        List<String> cmd, File cwd, Map<String,String> env, boolean redirect)
+    {
+        ProcessBuilder builder = new ProcessBuilder();
+
+        // fragile on Windows:
+        // see https://docs.microsoft.com/en-us/cpp/cpp/main-function-command-line-args?view=msvc-160
+        builder.command(cmd);
+
+        if (cwd != null) builder.directory(cwd);
+        if (env != null) {
+            builder.environment().clear();
+            builder.environment().putAll(env);
+        }
+        builder.redirectErrorStream(redirect);
+
+        return builder;
+    }
+
+    public static class Exec_Result
+    {
+        private final int _rc;
+        private final String _out;
+        private final String _err;
+
+        Exec_Result(int rc, String out, String err)
+        {
+            _rc = rc;
+            _out = out;
+            _err = err;
+        }
+
+        public int rc() { return _rc; }
+        public boolean ok() { return _rc == 0; }
+        public String out() { return _out; }
+        public String err() { return _err; }
+    }
+
+    public static Exec_Result exec_process(
+        List<String> command_line,
+        File cwd,
+        Map<String,String> env,
+        boolean redirect) throws IOException, InterruptedException
+    {
+        Path out_file = Files.createTempFile(null, null);
+        Path err_file = Files.createTempFile(null, null);
+        Exec_Result res;
+        try {
+            ProcessBuilder builder = process_builder(command_line, cwd, env, redirect);
+            builder.redirectOutput(out_file.toFile());
+            builder.redirectError(err_file.toFile());
+
+            Process proc = builder.start();
+            proc.getOutputStream().close();
+            try { proc.waitFor(); }
+            finally {
+                proc.getInputStream().close();
+                proc.getErrorStream().close();
+                proc.destroy();
+                Thread.interrupted();
+            }
+
+            int rc = proc.exitValue();
+            String out = Files.readString(out_file);
+            String err = Files.readString(err_file);
+            res = new Exec_Result(rc, out, err);
+        }
+        finally {
+            Files.deleteIfExists(out_file);
+            Files.deleteIfExists(err_file);
+        }
+        return res;
+    }
+
+
+    /* init (e.g. after extraction via 7zip) */
+
+    private static String bootstrap_directory(
+        String preference, String variable, String property, String description)
+    {
+        String a = preference;  // explicit argument
+        String b = System.getenv(variable);  // e.g. inherited from running isabelle tool
+        String c = System.getProperty(property);  // e.g. via JVM application boot process
+        String dir;
+
+        if (a != null && !a.isEmpty()) { dir = a; }
+        else if (b != null && !b.isEmpty()) { dir = b; }
+        else if (c != null && !c.isEmpty()) { dir = c; }
+        else { throw new RuntimeException("Unknown " + description + " directory"); }
+
+        if ((new File(dir)).isDirectory()) { return dir; }
+        else { throw new RuntimeException("Bad " + description + " directory " + quote(dir)); }
+    }
+
+    private static void cygwin_exec(String isabelle_root, List<String> cmd)
+        throws IOException, InterruptedException
+    {
+        File cwd = new File(isabelle_root);
+        Map<String,String> env = new HashMap<String,String>(System.getenv());
+        env.put("CYGWIN", "nodosfilewarning");
+        Exec_Result res = exec_process(cmd, cwd, env, true);
+        if (!res.ok()) throw new RuntimeException(res.out());
+    }
+
+    public static void cygwin_link(String content, File target) throws IOException
+    {
+        Path target_path = target.toPath();
+        Files.writeString(target_path, "!<symlink>" + content + "\u0000");
+        Files.setAttribute(target_path, "dos:system", true);
+    }
+
+    public static void cygwin_init(String isabelle_root, String cygwin_root)
+        throws IOException, InterruptedException
+    {
+        if (is_windows()) {
+            File uninitialized_file = new File(cygwin_root, "isabelle\\uninitialized");
+            boolean uninitialized = uninitialized_file.isFile() && uninitialized_file.delete();
+
+            if (uninitialized) {
+                Path symlinks_path = (new File(cygwin_root + "\\isabelle\\symlinks")).toPath();
+                String[] symlinks = Files.readAllLines(symlinks_path).toArray(new String[0]);
+
+                // recover symlinks
+                int i = 0;
+                int m = symlinks.length;
+                int n = (m > 0 && symlinks[m - 1].isEmpty()) ? m - 1 : m;
+                while (i < n) {
+                    if (i + 1 < n) {
+                        String target = symlinks[i];
+                        String content = symlinks[i + 1];
+                        cygwin_link(content, new File(isabelle_root, target));
+                        i += 2;
+                    } else { throw new RuntimeException("Unbalanced symlinks list"); }
+                }
+
+                cygwin_exec(isabelle_root,
+                    List.of(cygwin_root + "\\bin\\dash.exe", "/isabelle/rebaseall"));
+                cygwin_exec(isabelle_root,
+                    List.of(cygwin_root + "\\bin\\bash.exe", "/isabelle/postinstall"));
+            }
+        }
+    }
+
+
+    /* implicit settings environment */
+
+    private static volatile Map<String,String> _settings = null;
+
+    public static Map<String,String> settings()
+        throws IOException, InterruptedException
+    {
+        if (_settings == null) { init("", ""); }  // unsynchronized check
+        return _settings;
+    }
+
+    public static synchronized void init(String _isabelle_root, String _cygwin_root)
+        throws IOException, InterruptedException
+    {
+        if (_settings == null) {
+            String isabelle_root =
+                bootstrap_directory(_isabelle_root, "ISABELLE_ROOT", "isabelle.root", "Isabelle root");
+
+            String cygwin_root = "";
+            if (is_windows()) {
+                cygwin_root = bootstrap_directory(_cygwin_root, "CYGWIN_ROOT", "cygwin.root", "Cygwin root");
+                cygwin_init(isabelle_root, cygwin_root);
+            }
+
+            Map<String,String> env = new HashMap<String,String>(System.getenv());
+
+            BiFunction<String,String,Void> env_default =
+                (String a, String b) -> { if (!b.isEmpty()) env.putIfAbsent(a, b); return null; };
+
+            String temp_windows = is_windows() ? System.getenv("TEMP") : null;
+
+            env_default.apply("CYGWIN_ROOT", cygwin_root);
+            env_default.apply("TEMP_WINDOWS",
+                (temp_windows != null && temp_windows.contains("\\")) ? temp_windows : "");
+            env_default.apply("ISABELLE_JDK_HOME",
+                standard_path(cygwin_root, System.getProperty("java.home", "")));
+            env_default.apply("HOME", System.getProperty("user.home", ""));
+            env_default.apply("ISABELLE_APP", System.getProperty("isabelle.app", ""));
+
+            Map<String,String> settings = new HashMap<String,String>();
+            Path settings_file = Files.createTempFile(null, null);
+            try {
+                List<String> cmd = new LinkedList<String>();
+                if (is_windows()) {
+                    cmd.add(cygwin_root + "\\bin\\bash");
+                    cmd.add("-l");
+                    cmd.add(standard_path(cygwin_root, isabelle_root + "\\bin\\isabelle"));
+                } else {
+                    cmd.add(isabelle_root + "/bin/isabelle");
+                }
+                cmd.add("getenv");
+                cmd.add("-d");
+                cmd.add(settings_file.toString());
+
+                Exec_Result res = exec_process(cmd, null, env, true);
+                if (!res.ok()) throw new RuntimeException(res.out());
+
+                for (String s : Files.readString(settings_file).split("\u0000", -1)) {
+                    int i = s.indexOf('=');
+                    if (i > 0) { settings.put(s.substring(0, i), s.substring(i + 1)); }
+                    else if (i < 0 && !s.isEmpty()) { settings.put(s, ""); }
+                }
+            }
+            finally { Files.delete(settings_file); }
+
+            if (is_windows()) { settings.put("CYGWIN_ROOT", cygwin_root); }
+
+            settings.put("PATH", settings.get("PATH_JVM"));
+            settings.remove("PATH_JVM");
+
+            _settings = Map.copyOf(settings);
+        }
+    }
+}