diff --git a/tools/afp_build.scala b/tools/afp_build.scala --- a/tools/afp_build.scala +++ b/tools/afp_build.scala @@ -1,342 +1,333 @@ /* Author: Lars Hupel and Fabian Huch, TU Muenchen Unified AFP CI integration. */ package afp import isabelle.* import java.time.ZoneId import java.time.format.DateTimeFormatter object AFP_Build { /* mailing */ object Mailer { - def failed_subject(name: Metadata.Entry.Name): String = s"Build of AFP entry $name failed" + def failed_subject(name: Metadata.Entry.Name): String = "Build of AFP entry " + name + " failed" def failed_text(session_name: String, entry: Metadata.Entry.Name, isabelle_id: String, - afp_id: String, result: Process_Result): String = s""" + afp_id: String, result: Process_Result): String = """ The build for the session - $session_name, + """ + session_name + """", belonging to the AFP entry - $entry + """ + entry + """ failed. You are receiving this mail because you are the maintainer of that AFP entry. The following information might help you with resolving the problem. -Build log: ${Isabelle_System.getenv("BUILD_URL")} -Isabelle ID: $isabelle_id -AFP ID: $afp_id -Timeout? ${result.timeout} -Exit code: ${result.rc} +Build log: """ + Isabelle_System.getenv("BUILD_URL") + """ +Isabelle ID: """ + isabelle_id + """ +AFP ID: """ + afp_id + """ +Timeout? """ + result.timeout + """ +Exit code: """ + result.rc + """ Last 50 lines from stdout (if available): -${result.out_lines.takeRight(50).mkString("\n")} +""" + result.out_lines.takeRight(50).mkString("\n") + """" Last 50 lines from stderr (if available): -${result.err_lines.takeRight(50).mkString("\n")} -""" +""" + result.err_lines.takeRight(50).mkString("\n") + "\n" } /* metadata tooling */ class Metadata_Tools private( val structure: AFP_Structure, val server: Mail.Server, val entries: Metadata.Entries ) { def maintainers(name: Metadata.Entry.Name): List[Metadata.Email] = { entries.get(name) match { case None => Nil case Some(entry) => entry.notifies } } val sessions: Map[Metadata.Entry.Name, List[String]] = entries.values.map(entry => entry.name -> structure.entry_sessions(entry.name).map(_.name)).toMap def session_entry(session_name: String): Option[Metadata.Entry.Name] = { val entry = sessions.find { case (_, sessions) => sessions.contains(session_name) } entry.map { case (name, _) => name } } def by_entry(sessions: List[String]): Map[Option[Metadata.Entry.Name], List[String]] = sessions.groupBy(session_entry) def notify(name: Metadata.Entry.Name, subject: String, text: String): Boolean = { val recipients = entries.get(name).map(_.notifies).getOrElse(Nil) if (recipients.nonEmpty) { val from = Some(Mail.address("ci@isabelle.systems")) val to = recipients.map(mail => Mail.address(mail.address)) val mail = Mail(subject, to, text, from, "Jenkins Admin") server.send(mail) } recipients.nonEmpty } } object Metadata_Tools { def load(afp: AFP_Structure, options: Options): Metadata_Tools = new Metadata_Tools(afp, CI_Build.mail_server(options), afp.load()) } /* utilities */ - def status_as_json(isabelle_id: String, afp_id: String, start_time: String, - status: Map[Option[String], CI_Build.Status]): String = { - val entries_strings = status.collect { - case (Some(entry), status) => - s"""{"entry": "$entry", "status": "${status.str}"}""" - } - - val entries_string = entries_strings.mkString("[", ",\n", "]") + def status_as_json( + isabelle_id: String, + afp_id: String, + start_time: String, + status: Map[Option[String], CI_Build.Status] + ): JSON.T = { + val entries = + status.collect { + case (Some(entry), status) => JSON.Object("entry" -> entry, "status" -> status.str) + } - s""" - {"build_data": - {"isabelle_id": "$isabelle_id", - "afp_id": "$afp_id", - "time": "$start_time", - "url": "${Isabelle_System.getenv("BUILD_URL")}", - "job": "${Isabelle_System.getenv("JOB_NAME")}" - }, - "entries": - $entries_string - } - """ + JSON.Object( + "build_data" -> JSON.Object( + "isabelle_id" -> isabelle_id, + "afp_id" -> afp_id, + "time" -> start_time, + "url" -> Isabelle_System.getenv("BUILD_URL"), + "job" -> Isabelle_System.getenv("JOB_NAME")), + "entries" -> entries) } - def status_as_html(status: Map[Option[String], CI_Build.Status]): String = { - val entries_strings = status.collect { - case (None, CI_Build.Failed) => - s"
  • Distribution
  • " - case (Some(entry), CI_Build.Failed) => - s"""
  • $entry
  • """ - } + def status_as_html(status: Map[Option[String], CI_Build.Status]): XML.Body = { + val entries = + status.toList.collect { + case (None, CI_Build.Failed) => HTML.item(HTML.text("Distribution")) + case (Some(entry), CI_Build.Failed) => + HTML.item(List( + HTML.link("https://devel.isa-afp.org/entries/" + entry + ".html", + HTML.text(entry)))) + } - if (entries_strings.isEmpty) - "" - else - entries_strings.mkString("Failed entries: ") + if (entries.isEmpty) Nil + else HTML.text("Failed entries: ") ::: HTML.list(entries) :: Nil } /* ci build jobs */ val afp = - CI_Build.Job("afp", "builds the AFP, without slow sessions", CI_Build.Profile.from_host, - { - val afp = AFP_Structure() - - val status_file = Path.explode("$ISABELLE_HOME/status.json").file - val deps_file = Path.explode("$ISABELLE_HOME/dependencies.json").file + CI_Build.Job("afp", "builds the AFP, without slow sessions", CI_Build.Profile.from_host, { + val afp = AFP_Structure() - def pre_hook(options: Options): CI_Build.Result = { - println(s"AFP id ${ afp.hg_id }") - if (status_file.exists()) - status_file.delete() - CI_Build.Result.ok - } + val status_file = Path.explode("$ISABELLE_HOME/status.json").file + val deps_file = Path.explode("$ISABELLE_HOME/dependencies.json").file - def post_hook(results: Build.Results, options: Options, start_time: Time): CI_Build.Result = { - CI_Build.print_section("DEPENDENCIES") - println("Generating dependencies file ...") - val result = Isabelle_System.bash("isabelle afp_dependencies") - result.check - println("Writing dependencies file ...") - File.write(deps_file, result.out) - CI_Build.print_section("COMPLETED") + def pre_hook(options: Options): CI_Build.Result = { + println("AFP id " + afp.hg_id) + if (status_file.exists()) status_file.delete() + CI_Build.Result.ok + } + + def post_hook(results: Build.Results, options: Options, start_time: Time): CI_Build.Result = { + CI_Build.print_section("DEPENDENCIES") + println("Generating dependencies file ...") + val result = Isabelle_System.bash("isabelle afp_dependencies") + result.check + println("Writing dependencies file ...") + File.write(deps_file, result.out) + CI_Build.print_section("COMPLETED") + CI_Build.Result.ok + } + + CI_Build.Build_Config( + clean = false, include = List(afp.thys_dir), pre_hook = pre_hook, + post_hook = post_hook, selection = Sessions.Selection( + session_groups = List("AFP"), + exclude_session_groups = List("slow"))) + }) + + val all = + CI_Build.Job("all", "builds Isabelle + AFP (without slow)", CI_Build.Profile.from_host, { + val afp = AFP_Structure() + val isabelle_home = Path.explode(Isabelle_System.getenv_strict("ISABELLE_HOME")) + val isabelle_id = CI_Build.hg_id(isabelle_home) + + val status_file = Path.explode("$ISABELLE_HOME/status.json").file + val report_file = Path.explode("$ISABELLE_HOME/report.html").file + val deps_file = Path.explode("$ISABELLE_HOME/dependencies.json").file + + + def pre_hook(options: Options): CI_Build.Result = { + println("AFP id " + afp.hg_id) + if (status_file.exists()) status_file.delete() + if (report_file.exists()) report_file.delete() + + File.write(report_file, "") + + val server = CI_Build.mail_server(options) + if (!server.defined) { + println("Mail configuration not found.") + CI_Build.Result.error + } + else { + server.check() CI_Build.Result.ok } - - CI_Build.Build_Config( - clean = false, include = List(afp.thys_dir), pre_hook = pre_hook, - post_hook = post_hook, selection = Sessions.Selection( - session_groups = List("AFP"), - exclude_session_groups = List("slow"))) - }) - - val all = - CI_Build.Job("all", "builds Isabelle + AFP (without slow)", CI_Build.Profile.from_host, - { - val afp = AFP_Structure() - val isabelle_home = Path.explode(Isabelle_System.getenv_strict("ISABELLE_HOME")) - val isabelle_id = CI_Build.hg_id(isabelle_home) + } - val status_file = Path.explode("$ISABELLE_HOME/status.json").file - val report_file = Path.explode("$ISABELLE_HOME/report.html").file - val deps_file = Path.explode("$ISABELLE_HOME/dependencies.json").file - + def post_hook(results: Build.Results, options: Options, start_time: Time): CI_Build.Result = { + CI_Build.print_section("DEPENDENCIES") + println("Generating dependencies file ...") + val result = Isabelle_System.bash("isabelle afp_dependencies") + result.check - def pre_hook(options: Options): CI_Build.Result = { - println(s"AFP id ${ afp.hg_id }") - if (status_file.exists()) - status_file.delete() - if (report_file.exists()) - report_file.delete() + println("Writing dependencies file ...") + File.write(deps_file, result.out) - File.write(report_file, "") + val metadata = Metadata_Tools.load(afp, options) - val server = CI_Build.mail_server(options) - if (!server.defined) { - println(s"Mail configuration not found.") - CI_Build.Result.error - } - else { - server.check() - CI_Build.Result.ok + val status = metadata.by_entry(results.sessions.toList).view.mapValues { sessions => + CI_Build.Status.merge(sessions.map(CI_Build.Status.from_results(results, _))) + }.toMap + + CI_Build.print_section("REPORT") + println("Writing report file ...") + File.write(report_file, + HTML.output(status_as_html(status), hidden = false, structural = true)) + + CI_Build.print_section("SITEGEN") + println("Writing status file ...") + val formatted_time = + start_time.instant.atZone(ZoneId.systemDefault) + .format(DateTimeFormatter.RFC_1123_DATE_TIME) + File.write(status_file, + JSON.Format(status_as_json(isabelle_id, afp.hg_id, formatted_time, status))) + println("Running sitegen ...") + + val script = afp.base_dir + Path.explode("admin/sitegen-devel") + val sitegen_cmd = + Bash.strings(List(script.file.toString, status_file.toString, deps_file.toString)) + + val sitegen_res = + Isabelle_System.bash(sitegen_cmd, progress_stdout = println, progress_stderr = println) + if (!sitegen_res.ok) println("sitegen failed") + + if (!results.ok) { + CI_Build.print_section("NOTIFICATIONS") + for (session_name <- results.sessions) { + val result = results(session_name) + if (!result.ok && !results.cancelled(session_name) && metadata.server.defined) { + metadata.session_entry(session_name).foreach { entry => + val subject = Mailer.failed_subject(entry) + val text = Mailer.failed_text(session_name, entry, isabelle_id, afp.hg_id, result) + val notified = metadata.notify(entry, subject, text) + if (!notified) println("Entry " + entry + ": WARNING no maintainers specified") + } + } } } - def post_hook(results: Build.Results, options: Options, start_time: Time): CI_Build.Result = { - CI_Build.print_section("DEPENDENCIES") - println("Generating dependencies file ...") - val result = Isabelle_System.bash("isabelle afp_dependencies") - result.check - println("Writing dependencies file ...") - File.write(deps_file, result.out) - - val metadata = Metadata_Tools.load(afp, options) - - val status = metadata.by_entry(results.sessions.toList).view.mapValues { sessions => - CI_Build.Status.merge(sessions.map(CI_Build.Status.from_results(results, _))) - }.toMap - - CI_Build.print_section("REPORT") - println("Writing report file ...") - File.write(report_file, status_as_html(status)) - - CI_Build.print_section("SITEGEN") - println("Writing status file ...") - val formatted_time = - start_time.instant.atZone(ZoneId.systemDefault) - .format(DateTimeFormatter.RFC_1123_DATE_TIME) - File.write(status_file, status_as_json(isabelle_id, afp.hg_id, formatted_time, status)) - println("Running sitegen ...") - - val script = afp.base_dir + Path.explode("admin/sitegen-devel") - val sitegen_cmd = - Bash.strings(List(script.file.toString, status_file.toString, deps_file.toString)) + CI_Build.print_section("COMPLETED") + CI_Build.Result(sitegen_res.rc) + } - val sitegen_res = - Isabelle_System.bash(sitegen_cmd, progress_stdout = println, progress_stderr = println) - if (!sitegen_res.ok) { - println("sitegen failed") - } - - if (!results.ok) { - CI_Build.print_section("NOTIFICATIONS") - for (session_name <- results.sessions) { - val result = results(session_name) - if (!result.ok && !results.cancelled(session_name) && metadata.server.defined) { - metadata.session_entry(session_name).foreach { entry => - val subject = Mailer.failed_subject(entry) - val text = Mailer.failed_text(session_name, entry, isabelle_id, afp.hg_id, result) - val notified = metadata.notify(entry, subject, text) - if (!notified) println(s"Entry $entry: WARNING no maintainers specified") - } - } - } - } - - CI_Build.print_section("COMPLETED") - CI_Build.Result(sitegen_res.rc) - } - - CI_Build.Build_Config( - clean = false, include = List(afp.thys_dir), pre_hook = pre_hook, - post_hook = post_hook, selection = Sessions.Selection( - all_sessions = true, - exclude_session_groups = List("slow"))) - }) + CI_Build.Build_Config( + clean = false, include = List(afp.thys_dir), pre_hook = pre_hook, + post_hook = post_hook, selection = Sessions.Selection( + all_sessions = true, + exclude_session_groups = List("slow"))) + }) val mac = CI_Build.Job("mac", "builds the AFP (without some sessions) on Mac Os", - CI_Build.Profile.from_host.copy(threads = 8, jobs = 1), - { + CI_Build.Profile.from_host.copy(threads = 8, jobs = 1), { val afp = AFP_Structure() def pre_hook(options: Options): CI_Build.Result = { - println(s"Build for AFP id ${ afp.hg_id }") + println("Build for AFP id " + afp.hg_id) CI_Build.Result.ok } CI_Build.Build_Config( include = List(afp.thys_dir), pre_hook = pre_hook, selection = Sessions.Selection( all_sessions = true, - exclude_sessions = List("HOL-Proofs", "HOL-ODE-Numerics", "Linear_Programming", "HOL-Nominal-Examples", "HOL-Analysis"), + exclude_sessions = List("HOL-Proofs", "HOL-ODE-Numerics", "Linear_Programming", + "HOL-Nominal-Examples", "HOL-Analysis"), exclude_session_groups = List("slow"))) }) val slow = CI_Build.Job("slow", "builds the AFP slow sessions", - CI_Build.Profile.from_host.copy(threads = 8, jobs = 1), - { + CI_Build.Profile.from_host.copy(threads = 8, jobs = 1), { val afp = AFP_Structure() def pre_hook(options: Options): CI_Build.Result = { - println(s"Build for AFP id ${ afp.hg_id }") + println("Build for AFP id " + afp.hg_id) CI_Build.Result.ok } CI_Build.Build_Config( include = List(afp.thys_dir), pre_hook = pre_hook, selection = Sessions.Selection(session_groups = List("slow"))) }) val testboard = - CI_Build.Job("testboard", "builds the AFP testboard", CI_Build.Profile.from_host, - { - val afp = AFP_Structure() - val report_file = Path.explode("$ISABELLE_HOME/report.html").file - - def pre_hook(options: Options): CI_Build.Result = { - println(s"AFP id ${ afp.hg_id }") - if (report_file.exists()) - report_file.delete() - - File.write(report_file, "") - CI_Build.Result.ok - } - - def post_hook(results: Build.Results, options: Options, start_time: Time): CI_Build.Result = { - val metadata = Metadata_Tools.load(afp, options) + CI_Build.Job("testboard", "builds the AFP testboard", CI_Build.Profile.from_host, { + val afp = AFP_Structure() + val report_file = Path.explode("$ISABELLE_HOME/report.html").file - val status = metadata.by_entry(results.sessions.toList).view.mapValues { sessions => - CI_Build.Status.merge(sessions.map(CI_Build.Status.from_results(results, _))) - }.toMap + def pre_hook(options: Options): CI_Build.Result = { + println("AFP id " + afp.hg_id) + if (report_file.exists()) report_file.delete() - CI_Build.print_section("REPORT") - println("Writing report file ...") - File.write(report_file, status_as_html(status)) - CI_Build.print_section("COMPLETED") - CI_Build.Result.ok - } + File.write(report_file, "") + CI_Build.Result.ok + } - CI_Build.Build_Config( - clean = false, include = List(afp.thys_dir), pre_hook = pre_hook, - post_hook = post_hook, selection = - Sessions.Selection( - all_sessions = true, - exclude_session_groups = List("slow"))) - }) + def post_hook(results: Build.Results, options: Options, start_time: Time): CI_Build.Result = { + val metadata = Metadata_Tools.load(afp, options) + + val status = metadata.by_entry(results.sessions.toList).view.mapValues { sessions => + CI_Build.Status.merge(sessions.map(CI_Build.Status.from_results(results, _))) + }.toMap + + CI_Build.print_section("REPORT") + println("Writing report file ...") + File.write(report_file, + HTML.output(status_as_html(status), hidden = false, structural = true)) + CI_Build.print_section("COMPLETED") + CI_Build.Result.ok + } + + CI_Build.Build_Config( + clean = false, include = List(afp.thys_dir), pre_hook = pre_hook, + post_hook = post_hook, selection = + Sessions.Selection( + all_sessions = true, + exclude_session_groups = List("slow"))) + }) } class CI_Builds extends Isabelle_CI_Builds( AFP_Build.afp, AFP_Build.all, AFP_Build.slow, AFP_Build.mac, AFP_Build.testboard)