explicit indication of Admin tools;
authorwenzelm
Wed, 12 Oct 2016 10:22:34 +0200
changeset 64161 2b1128e95dfb
parent 64160 1eea419fab65
child 64162 03057a8fdd1f
explicit indication of Admin tools;
src/Pure/Admin/build_doc.scala
src/Pure/Admin/build_stats.scala
src/Pure/Admin/check_sources.scala
src/Pure/Admin/remote_dmg.scala
src/Pure/System/isabelle_system.scala
src/Pure/System/isabelle_tool.scala
src/Pure/Tools/build_doc.scala
src/Pure/Tools/build_stats.scala
src/Pure/Tools/check_sources.scala
src/Pure/Tools/remote_dmg.scala
src/Pure/build-jars
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/Pure/Admin/build_doc.scala	Wed Oct 12 10:22:34 2016 +0200
@@ -0,0 +1,107 @@
+/*  Title:      Pure/Admin/build_doc.scala
+    Author:     Makarius
+
+Build Isabelle documentation.
+*/
+
+package isabelle
+
+
+import java.io.{File => JFile}
+
+
+object Build_Doc
+{
+  /* build_doc */
+
+  def build_doc(
+    options: Options,
+    progress: Progress = Ignore_Progress,
+    all_docs: Boolean = false,
+    max_jobs: Int = 1,
+    system_mode: Boolean = false,
+    docs: List[String] = Nil): Int =
+  {
+    val selection =
+      for {
+        (name, info) <- Sessions.load(options).topological_order
+        if info.groups.contains("doc")
+        doc = info.options.string("document_variants")
+        if all_docs || docs.contains(doc)
+      } yield (doc, name)
+
+    val selected_docs = selection.map(_._1)
+    val sessions = selection.map(_._2)
+
+    docs.filter(doc => !selected_docs.contains(doc)) match {
+      case Nil =>
+      case bad => error("No documentation session for " + commas_quote(bad))
+    }
+
+    progress.echo("Build started for documentation " + commas_quote(selected_docs))
+
+    val res1 =
+      Build.build(options, progress, requirements = true, build_heap = true,
+        max_jobs = max_jobs, system_mode = system_mode, sessions = sessions)
+    if (res1.ok) {
+      Isabelle_System.with_tmp_dir("document_output")(output =>
+        {
+          val res2 =
+            Build.build(
+              options.bool.update("browser_info", false).
+                string.update("document", "pdf").
+                string.update("document_output", File.standard_path(output)),
+              progress, clean_build = true, max_jobs = max_jobs, system_mode = system_mode,
+              sessions = sessions)
+          if (res2.ok) {
+            val doc_dir = Path.explode("$ISABELLE_HOME/doc").file
+            for (doc <- selected_docs) {
+              val name = doc + ".pdf"
+              File.copy(new JFile(output, name), new JFile(doc_dir, name))
+            }
+          }
+          res2.rc
+        })
+    }
+    else res1.rc
+  }
+
+
+  /* Isabelle tool wrapper */
+
+  val isabelle_tool =
+    Isabelle_Tool("build_doc", "build Isabelle documentation", args =>
+    {
+      var all_docs = false
+      var max_jobs = 1
+      var system_mode = false
+
+      val getopts =
+        Getopts("""
+Usage: isabelle build_doc [OPTIONS] [DOCS ...]"
+
+  Options are:
+    -a           select all documentation sessions
+    -j INT       maximum number of parallel jobs (default 1)
+    -s           system build mode
+
+  Build Isabelle documentation from documentation sessions with
+  suitable document_variants entry.
+""",
+        "a" -> (_ => all_docs = true),
+        "j:" -> (arg => max_jobs = Value.Int.parse(arg)),
+        "s" -> (_ => system_mode = true))
+
+      val docs = getopts(args)
+
+      if (!all_docs && docs.isEmpty) getopts.usage()
+
+      val options = Options.init()
+      val progress = new Console_Progress()
+      val rc =
+        progress.interrupt_handler {
+          build_doc(options, progress, all_docs, max_jobs, system_mode, docs)
+        }
+      sys.exit(rc)
+    }, admin = true)
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/Pure/Admin/build_stats.scala	Wed Oct 12 10:22:34 2016 +0200
@@ -0,0 +1,191 @@
+/*  Title:      Pure/Admin/build_stats.scala
+    Author:     Makarius
+
+Statistics from session build output.
+*/
+
+package isabelle
+
+
+object Build_Stats
+{
+  /* presentation */
+
+  private val default_history_length = 100
+  private val default_size = (800, 600)
+  private val default_only_sessions = Set.empty[String]
+  private val default_elapsed_threshold = Time.zero
+  private val default_ml_timing: Option[Boolean] = None
+
+  def present_job(job: String, dir: Path,
+    history_length: Int = default_history_length,
+    size: (Int, Int) = default_size,
+    only_sessions: Set[String] = default_only_sessions,
+    elapsed_threshold: Time = default_elapsed_threshold,
+    ml_timing: Option[Boolean] = default_ml_timing): List[String] =
+  {
+    val job_infos = CI_API.build_job_builds(job).sortBy(_.timestamp).reverse.take(history_length)
+    if (job_infos.isEmpty) error("No build infos for job " + quote(job))
+
+    val all_infos =
+      Par_List.map((job_info: CI_API.Job_Info) =>
+        (job_info.timestamp / 1000, job_info.read_main_log.parse_build_info), job_infos)
+    val all_sessions =
+      (Set.empty[String] /: all_infos)(
+        { case (s, (_, info)) => s ++ info.sessions.keySet })
+
+    def check_threshold(info: Build_Log.Build_Info, session: String): Boolean =
+    {
+      val t = info.timing(session)
+      !t.is_zero && t.elapsed >= elapsed_threshold
+    }
+
+    val sessions =
+      for {
+        session <- (if (only_sessions.isEmpty) all_sessions else all_sessions & only_sessions)
+        if all_infos.filter({ case (_, info) => check_threshold(info, session) }).length >= 3
+      } yield session
+
+    Isabelle_System.mkdirs(dir)
+    for (session <- sessions) {
+      Isabelle_System.with_tmp_file(session, "png") { data_file =>
+        Isabelle_System.with_tmp_file(session, "gnuplot") { plot_file =>
+          val data =
+            for { (t, info) <- all_infos if info.finished(session) }
+            yield {
+              val timing1 = info.timing(session)
+              val timing2 = info.ml_timing(session)
+              List(t.toString,
+                timing1.elapsed.minutes,
+                timing1.cpu.minutes,
+                timing2.elapsed.minutes,
+                timing2.cpu.minutes,
+                timing2.gc.minutes).mkString(" ")
+            }
+          File.write(data_file, cat_lines(data))
+
+          val plots1 =
+            List(
+              """ using 1:3 smooth sbezier title "cpu time (smooth)" """,
+              """ using 1:3 smooth csplines title "cpu time" """,
+              """ using 1:2 smooth sbezier title "elapsed time (smooth)" """,
+              """ using 1:2 smooth csplines title "elapsed time" """)
+          val plots2 =
+            List(
+              """ using 1:5 smooth sbezier title "ML cpu time (smooth)" """,
+              """ using 1:5 smooth csplines title "ML cpu time" """,
+              """ using 1:4 smooth sbezier title "ML elapsed time (smooth)" """,
+              """ using 1:4 smooth csplines title "ML elapsed time" """,
+              """ using 1:6 smooth sbezier title "ML gc time (smooth)" """,
+              """ using 1:6 smooth csplines title "ML gc time" """)
+          val plots =
+            ml_timing match {
+              case None => plots1
+              case Some(false) => plots1 ::: plots2
+              case Some(true) => plots2
+            }
+
+          val data_file_name = File.standard_path(data_file.getAbsolutePath)
+          File.write(plot_file, """
+set terminal png size """ + size._1 + "," + size._2 + """
+set output """ + quote(File.standard_path(dir + Path.basic(session + ".png"))) + """
+set xdata time
+set timefmt "%s"
+set format x "%d-%b"
+set xlabel """ + quote(session) + """ noenhanced
+set key left top
+plot [] [0:] """ + plots.map(s => quote(data_file_name) + " " + s).mkString(", ") + "\n")
+          val result = Isabelle_System.bash("\"$ISABELLE_GNUPLOT\" " + File.bash_path(plot_file))
+          if (result.rc != 0) {
+            Output.error_message("Session " + session + ": gnuplot error")
+            result.print
+          }
+        }
+      }
+    }
+
+    sessions.toList.sorted
+  }
+
+
+  /* Isabelle tool wrapper */
+
+  private val html_header = """<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
+<html>
+<head><title>Performance statistics from session build output</title></head>
+<body>
+"""
+  private val html_footer = """
+</body>
+</html>
+"""
+
+  val isabelle_tool =
+    Isabelle_Tool("build_stats", "present statistics from session build output", args =>
+    {
+      var target_dir = Path.explode("stats")
+      var ml_timing = default_ml_timing
+      var only_sessions = default_only_sessions
+      var elapsed_threshold = default_elapsed_threshold
+      var history_length = default_history_length
+      var size = default_size
+
+      val getopts = Getopts("""
+Usage: isabelle build_stats [OPTIONS] [JOBS ...]
+
+  Options are:
+    -D DIR       target directory (default "stats")
+    -M           only ML timing
+    -S SESSIONS  only given SESSIONS (comma separated)
+    -T THRESHOLD only sessions with elapsed time >= THRESHOLD (minutes)
+    -l LENGTH    length of history (default 100)
+    -m           include ML timing
+    -s WxH       size of PNG image (default 800x600)
+
+  Present statistics from session build output of the given JOBS, from Jenkins
+  continuous build service specified as URL via ISABELLE_JENKINS_ROOT.
+""",
+        "D:" -> (arg => target_dir = Path.explode(arg)),
+        "M" -> (_ => ml_timing = Some(true)),
+        "S:" -> (arg => only_sessions = space_explode(',', arg).toSet),
+        "T:" -> (arg => elapsed_threshold = Time.minutes(Value.Double.parse(arg))),
+        "l:" -> (arg => history_length = Value.Int.parse(arg)),
+        "m" -> (_ => ml_timing = Some(false)),
+        "s:" -> (arg =>
+          space_explode('x', arg).map(Value.Int.parse(_)) match {
+            case List(w, h) if w > 0 && h > 0 => size = (w, h)
+            case _ => error("Error bad PNG image size: " + quote(arg))
+          }))
+
+      val jobs = getopts(args)
+      val all_jobs = CI_API.build_jobs()
+      val bad_jobs = jobs.filterNot(all_jobs.contains(_)).sorted
+
+      if (jobs.isEmpty)
+        error("No build jobs given. Available jobs: " + all_jobs.sorted.mkString(" "))
+
+      if (bad_jobs.nonEmpty)
+        error("Unknown build jobs: " + bad_jobs.mkString(" ") +
+          "\nAvailable jobs: " + all_jobs.sorted.mkString(" "))
+
+      for (job <- jobs) {
+        val dir = target_dir + Path.basic(job)
+        Output.writeln(dir.implode)
+        val sessions =
+          present_job(job, dir, history_length, size, only_sessions, elapsed_threshold, ml_timing)
+        File.write(dir + Path.basic("index.html"),
+          html_header + "\n<h1>" + HTML.output(job) + "</h1>\n" +
+          cat_lines(
+            sessions.map(session =>
+              """<br/><img src=""" + quote(HTML.output(session + ".png")) + """><br/>""")) +
+          "\n" + html_footer)
+      }
+
+      File.write(target_dir + Path.basic("index.html"),
+        html_header + "\n<ul>\n" +
+        cat_lines(
+          jobs.map(job => """<li> <a href=""" + quote(HTML.output(job + "/index.html")) + """>""" +
+            HTML.output(job) + """</a> </li>""")) +
+        "\n</ul>\n" + html_footer)
+  }, admin = true)
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/Pure/Admin/check_sources.scala	Wed Oct 12 10:22:34 2016 +0200
@@ -0,0 +1,72 @@
+/*  Title:      Pure/Admin/check_sources.scala
+    Author:     Makarius
+
+Some sanity checks for Isabelle sources.
+*/
+
+package isabelle
+
+
+object Check_Sources
+{
+  def check_file(path: Path)
+  {
+    val file_name = path.implode
+    val file_pos = path.position
+    def line_pos(i: Int) = Position.Line_File(i + 1, file_name)
+
+    val content = File.read(path)
+
+    for { (line, i) <- split_lines(content).iterator.zipWithIndex }
+    {
+      try {
+        Symbol.decode_strict(line)
+
+        for { c <- Word.codepoint_iterator(line); if c > 128 && !Character.isAlphabetic(c) }
+        {
+          Output.warning("Suspicious Unicode character " + quote(Word.codepoint(c)) +
+            Position.here(line_pos(i)))
+        }
+      }
+      catch { case ERROR(msg) => Output.warning(msg + Position.here(line_pos(i))) }
+
+      if (line.contains('\t'))
+        Output.warning("TAB character" + Position.here(line_pos(i)))
+    }
+
+    if (content.contains('\r'))
+      Output.warning("CR character" + Position.here(file_pos))
+
+    if (Word.bidi_detect(content))
+      Output.warning("Bidirectional Unicode text " + Position.here(file_pos))
+  }
+
+  def check_hg(root: Path)
+  {
+    Output.writeln("Checking " + root + " ...")
+    using(Mercurial.open_repository(root)) { hg =>
+      for {
+        file <- hg.manifest()
+        if file.endsWith(".thy") || file.endsWith(".ML") || file.endsWith("/ROOT")
+      } check_file(root + Path.explode(file))
+    }
+  }
+
+
+  /* Isabelle tool wrapper */
+
+  val isabelle_tool =
+    Isabelle_Tool("check_sources", "some sanity checks for Isabelle sources", args =>
+    {
+      val getopts = Getopts("""
+Usage: isabelle check_sources [ROOT_DIRS...]
+
+  Check .thy, .ML, ROOT files from manifest of Mercurial ROOT_DIRS.
+""")
+
+      val specs = getopts(args)
+      if (specs.isEmpty) getopts.usage()
+
+      for (root <- specs) check_hg(Path.explode(root))
+    }, admin = true)
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/Pure/Admin/remote_dmg.scala	Wed Oct 12 10:22:34 2016 +0200
@@ -0,0 +1,65 @@
+/*  Title:      Pure/Admin/remote_dmg.scala
+    Author:     Makarius
+
+Build dmg on remote Mac OS X system.
+*/
+
+package isabelle
+
+
+object Remote_DMG
+{
+  def remote_dmg(session: SSH.Session, tar_gz_file: Path, dmg_file: Path, volume_name: String = "")
+  {
+    session.with_tmp_dir(remote_dir =>
+      using(session.sftp())(sftp =>
+        {
+          val cd = "cd " + File.bash_string(remote_dir) + "; "
+
+          sftp.write_file(remote_dir + "/dmg.tar.gz", tar_gz_file)
+          session.execute(cd + "mkdir root && tar -C root -xzf dmg.tar.gz").check
+          session.execute(
+            cd + "hdiutil create -srcfolder root" +
+              (if (volume_name == "") "" else " -volname " + File.bash_string(volume_name)) +
+              " dmg.dmg").check
+          sftp.read_file(remote_dir + "/dmg.dmg", dmg_file)
+        }))
+  }
+
+
+  /* Isabelle tool wrapper */
+
+  val isabelle_tool =
+    Isabelle_Tool("remote_dmg", "build dmg on remote Mac OS X system", args =>
+    {
+      Command_Line.tool0 {
+        var port = SSH.default_port
+        var volume_name = ""
+
+        val getopts = Getopts("""
+Usage: isabelle remote_dmg [OPTIONS] USER@HOST TAR_GZ_FILE DMG_FILE
+
+  Options are:
+    -p PORT      alternative SSH port (default: """ + SSH.default_port + """)
+    -V NAME      specify volume name
+
+  Turn the contents of a tar.gz file into a dmg file -- produced on a remote
+  Mac OS X system.
+""",
+          "p:" -> (arg => port = Value.Int.parse(arg)),
+          "V:" -> (arg => volume_name = arg))
+
+        val more_args = getopts(args)
+        val (user, host, tar_gz_file, dmg_file) =
+          more_args match {
+            case List(SSH.User_Host(user, host), tar_gz_file, dmg_file) =>
+              (user, host, Path.explode(tar_gz_file), Path.explode(dmg_file))
+            case _ => getopts.usage()
+          }
+
+        val ssh = SSH.init(Options.init)
+        using(ssh.open_session(user = user, host = host, port = port))(
+          remote_dmg(_, tar_gz_file, dmg_file, volume_name))
+      }
+    }, admin = true)
+}
--- a/src/Pure/System/isabelle_system.scala	Wed Oct 12 09:38:20 2016 +0200
+++ b/src/Pure/System/isabelle_system.scala	Wed Oct 12 10:22:34 2016 +0200
@@ -324,6 +324,11 @@
 
   /** Isabelle resources **/
 
+  /* repository clone with Admin */
+
+  def admin(): Boolean = Path.explode("~~/Admin").is_dir
+
+
   /* components */
 
   def components(): List[Path] =
--- a/src/Pure/System/isabelle_tool.scala	Wed Oct 12 09:38:20 2016 +0200
+++ b/src/Pure/System/isabelle_tool.scala	Wed Oct 12 10:22:34 2016 +0200
@@ -113,11 +113,12 @@
       Update_Theorems.isabelle_tool)
 
   private def list_internal(): List[(String, String)] =
-    for (tool <- internal_tools.toList) yield (tool.name, tool.description)
+    for (tool <- internal_tools.toList if tool.accessible)
+      yield (tool.name, tool.description)
 
   private def find_internal(name: String): Option[List[String] => Unit] =
     internal_tools.collectFirst({
-      case tool if tool.name == name =>
+      case tool if tool.name == name && tool.accessible =>
         args => Command_Line.tool0 { tool.body(args) }
       })
 
@@ -148,4 +149,8 @@
   }
 }
 
-sealed case class Isabelle_Tool(name: String, description: String, body: List[String] => Unit)
+sealed case class Isabelle_Tool(
+  name: String, description: String, body: List[String] => Unit, admin: Boolean = false)
+{
+  def accessible: Boolean = !admin || Isabelle_System.admin()
+}
--- a/src/Pure/Tools/build_doc.scala	Wed Oct 12 09:38:20 2016 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,106 +0,0 @@
-/*  Title:      Pure/Tools/build_doc.scala
-    Author:     Makarius
-
-Build Isabelle documentation.
-*/
-
-package isabelle
-
-
-import java.io.{File => JFile}
-
-
-object Build_Doc
-{
-  /* build_doc */
-
-  def build_doc(
-    options: Options,
-    progress: Progress = Ignore_Progress,
-    all_docs: Boolean = false,
-    max_jobs: Int = 1,
-    system_mode: Boolean = false,
-    docs: List[String] = Nil): Int =
-  {
-    val selection =
-      for {
-        (name, info) <- Sessions.load(options).topological_order
-        if info.groups.contains("doc")
-        doc = info.options.string("document_variants")
-        if all_docs || docs.contains(doc)
-      } yield (doc, name)
-
-    val selected_docs = selection.map(_._1)
-    val sessions = selection.map(_._2)
-
-    docs.filter(doc => !selected_docs.contains(doc)) match {
-      case Nil =>
-      case bad => error("No documentation session for " + commas_quote(bad))
-    }
-
-    progress.echo("Build started for documentation " + commas_quote(selected_docs))
-
-    val res1 =
-      Build.build(options, progress, requirements = true, build_heap = true,
-        max_jobs = max_jobs, system_mode = system_mode, sessions = sessions)
-    if (res1.ok) {
-      Isabelle_System.with_tmp_dir("document_output")(output =>
-        {
-          val res2 =
-            Build.build(
-              options.bool.update("browser_info", false).
-                string.update("document", "pdf").
-                string.update("document_output", File.standard_path(output)),
-              progress, clean_build = true, max_jobs = max_jobs, system_mode = system_mode,
-              sessions = sessions)
-          if (res2.ok) {
-            val doc_dir = Path.explode("$ISABELLE_HOME/doc").file
-            for (doc <- selected_docs) {
-              val name = doc + ".pdf"
-              File.copy(new JFile(output, name), new JFile(doc_dir, name))
-            }
-          }
-          res2.rc
-        })
-    }
-    else res1.rc
-  }
-
-
-  /* Isabelle tool wrapper */
-
-  val isabelle_tool = Isabelle_Tool("build_doc", "build Isabelle documentation", args =>
-  {
-    var all_docs = false
-    var max_jobs = 1
-    var system_mode = false
-
-    val getopts =
-      Getopts("""
-Usage: isabelle build_doc [OPTIONS] [DOCS ...]"
-
-  Options are:
-    -a           select all documentation sessions
-    -j INT       maximum number of parallel jobs (default 1)
-    -s           system build mode
-
-  Build Isabelle documentation from documentation sessions with
-  suitable document_variants entry.
-""",
-      "a" -> (_ => all_docs = true),
-      "j:" -> (arg => max_jobs = Value.Int.parse(arg)),
-      "s" -> (_ => system_mode = true))
-
-    val docs = getopts(args)
-
-    if (!all_docs && docs.isEmpty) getopts.usage()
-
-    val options = Options.init()
-    val progress = new Console_Progress()
-    val rc =
-      progress.interrupt_handler {
-        build_doc(options, progress, all_docs, max_jobs, system_mode, docs)
-      }
-    sys.exit(rc)
-  })
-}
--- a/src/Pure/Tools/build_stats.scala	Wed Oct 12 09:38:20 2016 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,191 +0,0 @@
-/*  Title:      Pure/Tools/build_stats.scala
-    Author:     Makarius
-
-Statistics from session build output.
-*/
-
-package isabelle
-
-
-object Build_Stats
-{
-  /* presentation */
-
-  private val default_history_length = 100
-  private val default_size = (800, 600)
-  private val default_only_sessions = Set.empty[String]
-  private val default_elapsed_threshold = Time.zero
-  private val default_ml_timing: Option[Boolean] = None
-
-  def present_job(job: String, dir: Path,
-    history_length: Int = default_history_length,
-    size: (Int, Int) = default_size,
-    only_sessions: Set[String] = default_only_sessions,
-    elapsed_threshold: Time = default_elapsed_threshold,
-    ml_timing: Option[Boolean] = default_ml_timing): List[String] =
-  {
-    val job_infos = CI_API.build_job_builds(job).sortBy(_.timestamp).reverse.take(history_length)
-    if (job_infos.isEmpty) error("No build infos for job " + quote(job))
-
-    val all_infos =
-      Par_List.map((job_info: CI_API.Job_Info) =>
-        (job_info.timestamp / 1000, job_info.read_main_log.parse_build_info), job_infos)
-    val all_sessions =
-      (Set.empty[String] /: all_infos)(
-        { case (s, (_, info)) => s ++ info.sessions.keySet })
-
-    def check_threshold(info: Build_Log.Build_Info, session: String): Boolean =
-    {
-      val t = info.timing(session)
-      !t.is_zero && t.elapsed >= elapsed_threshold
-    }
-
-    val sessions =
-      for {
-        session <- (if (only_sessions.isEmpty) all_sessions else all_sessions & only_sessions)
-        if all_infos.filter({ case (_, info) => check_threshold(info, session) }).length >= 3
-      } yield session
-
-    Isabelle_System.mkdirs(dir)
-    for (session <- sessions) {
-      Isabelle_System.with_tmp_file(session, "png") { data_file =>
-        Isabelle_System.with_tmp_file(session, "gnuplot") { plot_file =>
-          val data =
-            for { (t, info) <- all_infos if info.finished(session) }
-            yield {
-              val timing1 = info.timing(session)
-              val timing2 = info.ml_timing(session)
-              List(t.toString,
-                timing1.elapsed.minutes,
-                timing1.cpu.minutes,
-                timing2.elapsed.minutes,
-                timing2.cpu.minutes,
-                timing2.gc.minutes).mkString(" ")
-            }
-          File.write(data_file, cat_lines(data))
-
-          val plots1 =
-            List(
-              """ using 1:3 smooth sbezier title "cpu time (smooth)" """,
-              """ using 1:3 smooth csplines title "cpu time" """,
-              """ using 1:2 smooth sbezier title "elapsed time (smooth)" """,
-              """ using 1:2 smooth csplines title "elapsed time" """)
-          val plots2 =
-            List(
-              """ using 1:5 smooth sbezier title "ML cpu time (smooth)" """,
-              """ using 1:5 smooth csplines title "ML cpu time" """,
-              """ using 1:4 smooth sbezier title "ML elapsed time (smooth)" """,
-              """ using 1:4 smooth csplines title "ML elapsed time" """,
-              """ using 1:6 smooth sbezier title "ML gc time (smooth)" """,
-              """ using 1:6 smooth csplines title "ML gc time" """)
-          val plots =
-            ml_timing match {
-              case None => plots1
-              case Some(false) => plots1 ::: plots2
-              case Some(true) => plots2
-            }
-
-          val data_file_name = File.standard_path(data_file.getAbsolutePath)
-          File.write(plot_file, """
-set terminal png size """ + size._1 + "," + size._2 + """
-set output """ + quote(File.standard_path(dir + Path.basic(session + ".png"))) + """
-set xdata time
-set timefmt "%s"
-set format x "%d-%b"
-set xlabel """ + quote(session) + """ noenhanced
-set key left top
-plot [] [0:] """ + plots.map(s => quote(data_file_name) + " " + s).mkString(", ") + "\n")
-          val result = Isabelle_System.bash("\"$ISABELLE_GNUPLOT\" " + File.bash_path(plot_file))
-          if (result.rc != 0) {
-            Output.error_message("Session " + session + ": gnuplot error")
-            result.print
-          }
-        }
-      }
-    }
-
-    sessions.toList.sorted
-  }
-
-
-  /* Isabelle tool wrapper */
-
-  private val html_header = """<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
-<html>
-<head><title>Performance statistics from session build output</title></head>
-<body>
-"""
-  private val html_footer = """
-</body>
-</html>
-"""
-
-  val isabelle_tool =
-    Isabelle_Tool("build_stats", "present statistics from session build output", args =>
-    {
-      var target_dir = Path.explode("stats")
-      var ml_timing = default_ml_timing
-      var only_sessions = default_only_sessions
-      var elapsed_threshold = default_elapsed_threshold
-      var history_length = default_history_length
-      var size = default_size
-
-      val getopts = Getopts("""
-Usage: isabelle build_stats [OPTIONS] [JOBS ...]
-
-  Options are:
-    -D DIR       target directory (default "stats")
-    -M           only ML timing
-    -S SESSIONS  only given SESSIONS (comma separated)
-    -T THRESHOLD only sessions with elapsed time >= THRESHOLD (minutes)
-    -l LENGTH    length of history (default 100)
-    -m           include ML timing
-    -s WxH       size of PNG image (default 800x600)
-
-  Present statistics from session build output of the given JOBS, from Jenkins
-  continuous build service specified as URL via ISABELLE_JENKINS_ROOT.
-""",
-        "D:" -> (arg => target_dir = Path.explode(arg)),
-        "M" -> (_ => ml_timing = Some(true)),
-        "S:" -> (arg => only_sessions = space_explode(',', arg).toSet),
-        "T:" -> (arg => elapsed_threshold = Time.minutes(Value.Double.parse(arg))),
-        "l:" -> (arg => history_length = Value.Int.parse(arg)),
-        "m" -> (_ => ml_timing = Some(false)),
-        "s:" -> (arg =>
-          space_explode('x', arg).map(Value.Int.parse(_)) match {
-            case List(w, h) if w > 0 && h > 0 => size = (w, h)
-            case _ => error("Error bad PNG image size: " + quote(arg))
-          }))
-
-      val jobs = getopts(args)
-      val all_jobs = CI_API.build_jobs()
-      val bad_jobs = jobs.filterNot(all_jobs.contains(_)).sorted
-
-      if (jobs.isEmpty)
-        error("No build jobs given. Available jobs: " + all_jobs.sorted.mkString(" "))
-
-      if (bad_jobs.nonEmpty)
-        error("Unknown build jobs: " + bad_jobs.mkString(" ") +
-          "\nAvailable jobs: " + all_jobs.sorted.mkString(" "))
-
-      for (job <- jobs) {
-        val dir = target_dir + Path.basic(job)
-        Output.writeln(dir.implode)
-        val sessions =
-          present_job(job, dir, history_length, size, only_sessions, elapsed_threshold, ml_timing)
-        File.write(dir + Path.basic("index.html"),
-          html_header + "\n<h1>" + HTML.output(job) + "</h1>\n" +
-          cat_lines(
-            sessions.map(session =>
-              """<br/><img src=""" + quote(HTML.output(session + ".png")) + """><br/>""")) +
-          "\n" + html_footer)
-      }
-
-      File.write(target_dir + Path.basic("index.html"),
-        html_header + "\n<ul>\n" +
-        cat_lines(
-          jobs.map(job => """<li> <a href=""" + quote(HTML.output(job + "/index.html")) + """>""" +
-            HTML.output(job) + """</a> </li>""")) +
-        "\n</ul>\n" + html_footer)
-  })
-}
--- a/src/Pure/Tools/check_sources.scala	Wed Oct 12 09:38:20 2016 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,72 +0,0 @@
-/*  Title:      Pure/Tools/check_sources.scala
-    Author:     Makarius
-
-Some sanity checks for Isabelle sources.
-*/
-
-package isabelle
-
-
-object Check_Sources
-{
-  def check_file(path: Path)
-  {
-    val file_name = path.implode
-    val file_pos = path.position
-    def line_pos(i: Int) = Position.Line_File(i + 1, file_name)
-
-    val content = File.read(path)
-
-    for { (line, i) <- split_lines(content).iterator.zipWithIndex }
-    {
-      try {
-        Symbol.decode_strict(line)
-
-        for { c <- Word.codepoint_iterator(line); if c > 128 && !Character.isAlphabetic(c) }
-        {
-          Output.warning("Suspicious Unicode character " + quote(Word.codepoint(c)) +
-            Position.here(line_pos(i)))
-        }
-      }
-      catch { case ERROR(msg) => Output.warning(msg + Position.here(line_pos(i))) }
-
-      if (line.contains('\t'))
-        Output.warning("TAB character" + Position.here(line_pos(i)))
-    }
-
-    if (content.contains('\r'))
-      Output.warning("CR character" + Position.here(file_pos))
-
-    if (Word.bidi_detect(content))
-      Output.warning("Bidirectional Unicode text " + Position.here(file_pos))
-  }
-
-  def check_hg(root: Path)
-  {
-    Output.writeln("Checking " + root + " ...")
-    using(Mercurial.open_repository(root)) { hg =>
-      for {
-        file <- hg.manifest()
-        if file.endsWith(".thy") || file.endsWith(".ML") || file.endsWith("/ROOT")
-      } check_file(root + Path.explode(file))
-    }
-  }
-
-
-  /* Isabelle tool wrapper */
-
-  val isabelle_tool =
-    Isabelle_Tool("check_sources", "some sanity checks for Isabelle sources", args =>
-    {
-      val getopts = Getopts("""
-Usage: isabelle check_sources [ROOT_DIRS...]
-
-  Check .thy, .ML, ROOT files from manifest of Mercurial ROOT_DIRS.
-""")
-
-      val specs = getopts(args)
-      if (specs.isEmpty) getopts.usage()
-
-      for (root <- specs) check_hg(Path.explode(root))
-    })
-}
--- a/src/Pure/Tools/remote_dmg.scala	Wed Oct 12 09:38:20 2016 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,64 +0,0 @@
-/*  Title:      Pure/Tools/remote_dmg.scala
-    Author:     Makarius
-
-Build dmg on remote Mac OS X system.
-*/
-
-package isabelle
-
-
-object Remote_DMG
-{
-  def remote_dmg(session: SSH.Session, tar_gz_file: Path, dmg_file: Path, volume_name: String = "")
-  {
-    session.with_tmp_dir(remote_dir =>
-      using(session.sftp())(sftp =>
-        {
-          val cd = "cd " + File.bash_string(remote_dir) + "; "
-
-          sftp.write_file(remote_dir + "/dmg.tar.gz", tar_gz_file)
-          session.execute(cd + "mkdir root && tar -C root -xzf dmg.tar.gz").check
-          session.execute(
-            cd + "hdiutil create -srcfolder root" +
-              (if (volume_name == "") "" else " -volname " + File.bash_string(volume_name)) +
-              " dmg.dmg").check
-          sftp.read_file(remote_dir + "/dmg.dmg", dmg_file)
-        }))
-  }
-
-
-  /* Isabelle tool wrapper */
-
-  val isabelle_tool = Isabelle_Tool("remote_dmg", "build dmg on remote Mac OS X system", args =>
-  {
-    Command_Line.tool0 {
-      var port = SSH.default_port
-      var volume_name = ""
-
-      val getopts = Getopts("""
-Usage: isabelle remote_dmg [OPTIONS] USER@HOST TAR_GZ_FILE DMG_FILE
-
-  Options are:
-    -p PORT      alternative SSH port (default: """ + SSH.default_port + """)
-    -V NAME      specify volume name
-
-  Turn the contents of a tar.gz file into a dmg file -- produced on a remote
-  Mac OS X system.
-""",
-        "p:" -> (arg => port = Value.Int.parse(arg)),
-        "V:" -> (arg => volume_name = arg))
-
-      val more_args = getopts(args)
-      val (user, host, tar_gz_file, dmg_file) =
-        more_args match {
-          case List(SSH.User_Host(user, host), tar_gz_file, dmg_file) =>
-            (user, host, Path.explode(tar_gz_file), Path.explode(dmg_file))
-          case _ => getopts.usage()
-        }
-
-      val ssh = SSH.init(Options.init)
-      using(ssh.open_session(user = user, host = host, port = port))(
-        remote_dmg(_, tar_gz_file, dmg_file, volume_name))
-    }
-  })
-}
--- a/src/Pure/build-jars	Wed Oct 12 09:38:20 2016 +0200
+++ b/src/Pure/build-jars	Wed Oct 12 10:22:34 2016 +0200
@@ -9,11 +9,15 @@
 ## sources
 
 declare -a SOURCES=(
+  Admin/build_doc.scala
   Admin/build_history.scala
   Admin/build_log.scala
+  Admin/build_stats.scala
+  Admin/check_sources.scala
   Admin/ci_api.scala
   Admin/ci_profile.scala
   Admin/isabelle_cronjob.scala
+  Admin/remote_dmg.scala
   Concurrent/consumer_thread.scala
   Concurrent/counter.scala
   Concurrent/event_timer.scala
@@ -113,10 +117,7 @@
   Thy/thy_syntax.scala
   Tools/bibtex.scala
   Tools/build.scala
-  Tools/build_doc.scala
-  Tools/build_stats.scala
   Tools/check_keywords.scala
-  Tools/check_sources.scala
   Tools/debugger.scala
   Tools/doc.scala
   Tools/main.scala
@@ -125,7 +126,6 @@
   Tools/ml_statistics.scala
   Tools/news.scala
   Tools/print_operation.scala
-  Tools/remote_dmg.scala
   Tools/simplifier_trace.scala
   Tools/task_statistics.scala
   Tools/update_cartouches.scala