diff --git a/tools/afp_build.scala b/tools/afp_build.scala
--- a/tools/afp_build.scala
+++ b/tools/afp_build.scala
@@ -1,392 +1,370 @@
/* Author: Lars Hupel and Fabian Huch, TU Muenchen
Unified AFP CI integration.
*/
package afp
import isabelle._
import isabelle.CI_Build._
import afp.Metadata.{Email, Entry}
import java.time.{Instant, ZoneId}
import java.time.format.DateTimeFormatter
import java.util.{Map => JMap, Properties => JProperties}
import javax.mail.internet.{InternetAddress, MimeMessage}
import javax.mail.{Authenticator, Message, MessagingException, PasswordAuthentication, Transport, Session => JSession}
import scala.sys.process._
object AFP_Build {
/* mailing */
case class Mail(subject: String, recipients: List[String], text: String) {
def send(): Unit = {
val user = System.getProperty("mail.smtp.user")
val sender = System.getProperty("mail.smtp.from")
val password = System.getProperty("mail.smtp.password")
System.setProperty("mail.smtp.ssl.protocols", "TLSv1.2")
val authenticator = new Authenticator() {
override def getPasswordAuthentication =
new PasswordAuthentication(user, password)
}
val session = JSession.getDefaultInstance(System.getProperties, authenticator)
val message = new MimeMessage(session)
message.setFrom(new InternetAddress("ci@isabelle.systems", "Isabelle/Jenkins"))
message.setSender(new InternetAddress(sender))
message.setSubject(subject)
message.setText(text, "UTF-8")
message.setSentDate(new java.util.Date())
recipients.foreach { recipient =>
message.addRecipient(Message.RecipientType.TO, new InternetAddress(recipient))
}
try {
Transport.send(message)
} catch {
case ex: MessagingException => println(s"Sending mail failed: ${ex.getMessage}")
}
}
}
object Mail {
def apply(subject: String, recipients: List[String], text: String) =
new Mail(subject, recipients, text)
def failed_subject(name: Entry.Name): String = s"Build of AFP entry $name failed"
def failed_text(session_name: String, entry: Entry.Name, isabelle_id: String,
afp_id: String, result: Process_Result): String = s"""
The build for the session
$session_name,
belonging to the AFP 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}
Last 50 lines from stdout (if available):
${result.out_lines.takeRight(50).mkString("\n")}
Last 50 lines from stderr (if available):
${result.err_lines.takeRight(50).mkString("\n")}
"""
}
/* metadata tooling */
case class Metadata_Tools private(structure: AFP_Structure, entries: Map[Entry.Name, Entry]) {
def maintainers(name: Entry.Name): List[Email] = {
entries.get(name) match {
case None => Nil
case Some(entry) => entry.notifies
}
}
val sessions: Map[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[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[Entry.Name], List[String]] =
sessions.groupBy(session_entry)
def notify(name: Entry.Name, subject: String, text: String): Boolean = {
val recipients = entries.get(name).map(_.notifies).getOrElse(Nil)
if (recipients.nonEmpty) Mail(subject, recipients.map(_.address), text).send()
recipients.nonEmpty
}
}
object Metadata_Tools {
def apply(structure: AFP_Structure, entries: List[Entry]): Metadata_Tools =
new Metadata_Tools(structure, entries.map(entry => entry.name -> entry).toMap)
def load(afp: AFP_Structure): Metadata_Tools = Metadata_Tools(afp, afp.load())
}
/* utilities */
def status_as_json(isabelle_id: String, afp_id: String, start_time: String,
status: Map[Option[String], Status]): String = {
val entries_strings = status.collect {
case (Some(entry), status) =>
s"""{"entry": "$entry", "status": "${status.str}"}"""
}
val entries_string = entries_strings.mkString("[", ",\n", "]")
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
}
"""
}
def status_as_html(status: Map[Option[String], Status]): String = {
val entries_strings = status.collect {
case (None, Failed) =>
s"
Distribution"
case (Some(entry), Failed) =>
s"""$entry"""
}
if (entries_strings.isEmpty)
""
else
entries_strings.mkString("Failed entries: ")
}
- private def load_properties(): JProperties = {
- val props = new JProperties()
- val file_name = Isabelle_System.getenv("ISABELLE_CI_PROPERTIES")
-
- if (file_name != "") {
- val file = Path.explode(file_name).file
- if (file.exists())
- props.load(new java.io.FileReader(file))
- props
- }
- else
- props
- }
-
- private def compute_timing(results: Build.Results, group: Option[String]): Timing = {
- val timings = results.sessions.collect {
- case session if group.forall(results.info(session).groups.contains(_)) =>
- results(session).timing
- }
- timings.foldLeft(Timing.zero)(_ + _)
- }
-
/* ci build jobs */
val afp =
Job("afp", "builds the AFP, without slow sessions", 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
def pre_hook(): Result = {
println(s"AFP id ${ afp.hg_id }")
if (status_file.exists())
status_file.delete()
Result.ok
}
def post_hook(results: Build.Results, start_time: Time): Result = {
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)
print_section("COMPLETED")
Result.ok
}
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 =
Job("all", "builds Isabelle + AFP (without slow)", Profile.from_host,
{
val afp = AFP_Structure()
val isabelle_home = Path.explode(Isabelle_System.getenv_strict("ISABELLE_HOME"))
val isabelle_id = 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 can_send_mails = System.getProperties.containsKey("mail.smtp.host")
def pre_hook(): Result = {
println(s"AFP id ${ afp.hg_id }")
if (status_file.exists())
status_file.delete()
if (report_file.exists())
report_file.delete()
File.write(report_file, "")
if (!can_send_mails) {
println(s"Mail configuration not found.")
Result.error
} else {
Result.ok
}
}
def post_hook(results: Build.Results, start_time: Time): Result = {
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)
val status = metadata.by_entry(results.sessions.toList).view.mapValues { sessions =>
Status.merge(sessions.map(Status.from_results(results, _)))
}.toMap
print_section("REPORT")
println("Writing report file ...")
File.write(report_file, status_as_html(status))
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_rc = List(script.file.toString, status_file.toString, deps_file.toString).!
if (sitegen_rc > 0) {
println("sitegen failed")
}
if (!results.ok) {
print_section("NOTIFICATIONS")
for (session_name <- results.sessions) {
val result = results(session_name)
if (!result.ok && !results.cancelled(session_name) && can_send_mails) {
metadata.session_entry(session_name).foreach { entry =>
val subject = Mail.failed_subject(entry)
val text = Mail.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")
}
}
}
}
print_section("COMPLETED")
Result(sitegen_rc)
}
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 =
Job("mac", "builds the AFP (without some sessions) on Mac Os",
Profile.from_host.copy(threads = 8, jobs = 1),
{
val afp = AFP_Structure()
def pre_hook(): Result = {
println(s"Build for AFP id ${ afp.hg_id }")
Result.ok
}
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_session_groups = List("slow")))
})
val slow =
Job("slow", "builds the AFP slow sessions", Profile.from_host.copy(threads = 8, jobs = 1),
{
val afp = AFP_Structure()
def pre_hook(): Result = {
println(s"Build for AFP id ${ afp.hg_id }")
Result.ok
}
Build_Config(
include = List(afp.thys_dir), pre_hook = pre_hook,
selection = Sessions.Selection(session_groups = List("slow")))
})
val testboard =
Job("testboard", "builds the AFP testboard", Profile.from_host,
{
val afp = AFP_Structure()
val report_file = Path.explode("$ISABELLE_HOME/report.html").file
def pre_hook(): Result = {
println(s"AFP id ${ afp.hg_id }")
if (report_file.exists())
report_file.delete()
File.write(report_file, "")
Result.ok
}
def post_hook(results: Build.Results, start_time: Time): Result = {
val metadata = Metadata_Tools.load(afp)
val status = metadata.by_entry(results.sessions.toList).view.mapValues { sessions =>
Status.merge(sessions.map(Status.from_results(results, _)))
}.toMap
print_section("REPORT")
println("Writing report file ...")
File.write(report_file, status_as_html(status))
print_section("COMPLETED")
Result.ok
}
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)