diff --git a/migrations/20250703173258_index.sql b/migrations/20250703173258_index.sql
new file mode 100644
index 0000000..ee4693a
--- /dev/null
+++ b/migrations/20250703173258_index.sql
@@ -0,0 +1,5 @@
+-- Add migration script here
+
+CREATE INDEX IF NOT EXISTS build_info_nightly ON build_info (nightly);
+
+CREATE INDEX IF NOT EXISTS build_info_target ON build_info (target);
diff --git a/migrations/20250703200547_broken-error.sql b/migrations/20250703200547_broken-error.sql
new file mode 100644
index 0000000..8f6dfde
--- /dev/null
+++ b/migrations/20250703200547_broken-error.sql
@@ -0,0 +1,4 @@
+-- Add migration script here
+
+ALTER TABLE finished_nightly
+ ADD COLUMN broken_error BOOLEAN DEFAULT NULL;
diff --git a/src/build.rs b/src/build.rs
index 9fcb5b3..32fa1e1 100644
--- a/src/build.rs
+++ b/src/build.rs
@@ -57,19 +57,19 @@ async fn background_builder_inner(db: &Db, nightly_cache: &mut NightlyCache) ->
match next {
Some((nightly, mode)) => {
info!(%nightly, %mode, "Building next nightly");
- let result = build_every_target_for_toolchain(&db, &nightly, mode)
+ let result = build_every_target_for_toolchain(db, &nightly, mode)
.await
.wrap_err_with(|| format!("building targets for toolchain {nightly}"));
if let Err(err) = result {
error!(%nightly, %mode, ?err, "Failed to build nightly");
- db.finish_nightly_as_broken(&nightly, mode)
+ db.finish_nightly_as_broken(&nightly, mode, &format!("{err:?}"))
.await
.wrap_err("marking nightly as broken")?;
}
}
None => {
info!("No new nightly, waiting for an hour to try again");
- tokio::time::sleep(Duration::from_secs(1 * 60 * 60)).await;
+ tokio::time::sleep(Duration::from_secs(60 * 60)).await;
}
}
Ok(())
@@ -191,7 +191,7 @@ pub async fn build_every_target_for_toolchain(
let results = futures::stream::iter(
targets
.iter()
- .map(|target| build_single_target(&db, nightly, target, mode)),
+ .map(|target| build_single_target(db, nightly, target, mode)),
)
.buffer_unordered(concurrent)
.collect::>>()
@@ -266,7 +266,7 @@ async fn build_target(
BuildMode::Core => {
let init = Command::new("cargo")
.args(["init", "--lib", "--name", "target-test"])
- .current_dir(&tmpdir)
+ .current_dir(tmpdir)
.output()
.await
.wrap_err("spawning cargo init")?;
@@ -282,7 +282,7 @@ async fn build_target(
.arg(format!("+{toolchain}"))
.args(["build", "-Zbuild-std=core", "--release"])
.args(["--target", target])
- .current_dir(&tmpdir)
+ .current_dir(tmpdir)
.output()
.await
.wrap_err("spawning cargo build")?
@@ -291,7 +291,7 @@ async fn build_target(
.arg(format!("+{toolchain}"))
.args(["miri", "setup"])
.args(["--target", target])
- .current_dir(&tmpdir)
+ .current_dir(tmpdir)
.env("MIRI_SYSROOT", tmpdir)
.output()
.await
diff --git a/src/db.rs b/src/db.rs
index 75a87a4..42c1811 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -33,7 +33,7 @@ impl Display for BuildMode {
}
}
-#[derive(sqlx::FromRow, Serialize, Deserialize)]
+#[derive(sqlx::FromRow, Serialize, Deserialize, Clone)]
pub struct BuildInfo {
pub nightly: String,
pub target: String,
@@ -73,6 +73,14 @@ pub struct FinishedNightly {
pub mode: BuildMode,
}
+#[derive(sqlx::FromRow, Debug, PartialEq, Eq, Hash)]
+pub struct FinishedNightlyWithBroken {
+ pub nightly: String,
+ pub mode: BuildMode,
+ pub is_broken: bool,
+ pub broken_error: Option,
+}
+
impl Db {
pub async fn open(path: &str) -> Result {
let db_opts = SqliteConnectOptions::from_str(path)
@@ -81,7 +89,7 @@ impl Db {
let conn = Pool::connect_with(db_opts)
.await
- .wrap_err_with(|| format!("opening db from `{}`", path))?;
+ .wrap_err_with(|| format!("opening db from `{path}`"))?;
Ok(Self { conn })
}
@@ -100,13 +108,6 @@ impl Db {
Ok(())
}
- pub async fn full_mega_monster(&self) -> Result> {
- sqlx::query_as::<_, BuildInfo>("SELECT nightly, target, status, mode FROM build_info")
- .fetch_all(&self.conn)
- .await
- .wrap_err("getting build status from DB")
- }
-
pub async fn history_for_target(&self, target: &str) -> Result> {
sqlx::query_as::<_, BuildInfo>(
"SELECT nightly, target, status, mode FROM build_info WHERE target = ?",
@@ -117,6 +118,26 @@ impl Db {
.wrap_err("getting history for single target")
}
+ pub async fn history_for_nightly(&self, nightly: &str) -> Result> {
+ sqlx::query_as::<_, BuildInfo>(
+ "SELECT nightly, target, status, mode FROM build_info WHERE nightly = ?",
+ )
+ .bind(nightly)
+ .fetch_all(&self.conn)
+ .await
+ .wrap_err("getting history for single nightly")
+ }
+
+ pub async fn nightly_info(&self, nightly: &str) -> Result> {
+ sqlx::query_as::<_, FinishedNightlyWithBroken>(
+ "SELECT nightly, mode, is_broken, broken_error FROM finished_nightly WHERE nightly = ?",
+ )
+ .bind(nightly)
+ .fetch_all(&self.conn)
+ .await
+ .wrap_err("getting finished_nightly for single nightly")
+ }
+
pub async fn target_list(&self) -> Result> {
#[derive(sqlx::FromRow)]
struct TargetName {
@@ -130,6 +151,21 @@ impl Db {
.map(|elems| elems.into_iter().map(|elem| elem.target).collect())
}
+ pub async fn nightly_list(&self) -> Result> {
+ #[derive(sqlx::FromRow)]
+ struct NightlyName {
+ nightly: String,
+ }
+
+ sqlx::query_as::<_, NightlyName>(
+ "SELECT DISTINCT nightly FROM build_info ORDER BY nightly DESC",
+ )
+ .fetch_all(&self.conn)
+ .await
+ .wrap_err("getting list of all targets")
+ .map(|elems| elems.into_iter().map(|elem| elem.nightly).collect())
+ }
+
pub async fn build_status_full(
&self,
nightly: &str,
@@ -186,10 +222,16 @@ impl Db {
Ok(())
}
- pub async fn finish_nightly_as_broken(&self, nightly: &str, mode: BuildMode) -> Result<()> {
- sqlx::query("INSERT INTO finished_nightly (nightly, mode, is_broken) VALUES (?, ?, TRUE)")
+ pub async fn finish_nightly_as_broken(
+ &self,
+ nightly: &str,
+ mode: BuildMode,
+ error: &str,
+ ) -> Result<()> {
+ sqlx::query("INSERT INTO finished_nightly (nightly, mode, is_broken, broken_error) VALUES (?, ?, TRUE, ?)")
.bind(nightly)
.bind(mode)
+ .bind(error)
.execute(&self.conn)
.await
.wrap_err("inserting finished broken nightly")?;
diff --git a/src/nightlies.rs b/src/nightlies.rs
index 7493870..f6675dd 100644
--- a/src/nightlies.rs
+++ b/src/nightlies.rs
@@ -9,7 +9,7 @@ use tracing::debug;
use crate::db::{BuildMode, FinishedNightly};
-const EARLIEST_CUTOFF_DATE: &str = "2023-01-01";
+const EARLIEST_CUTOFF_DATE: &str = "2022-01-01";
#[derive(Default)]
pub struct NightlyCache {
@@ -43,7 +43,7 @@ impl Nightlies {
.last()
.ok_or_eyre("did not find any nightlies in manifests.txt")?;
- for nightly in guess_more_recent_nightlies(&latest)? {
+ for nightly in guess_more_recent_nightlies(latest)? {
if nightly_exists(&nightly, cache)
.await
.wrap_err_with(|| format!("checking whether {nightly} exists"))?
@@ -54,7 +54,10 @@ impl Nightlies {
all.reverse();
- debug!("Loaded {} nightlies from the manifest and manual additions", all.len());
+ debug!(
+ "Loaded {} nightlies from the manifest and manual additions",
+ all.len()
+ );
Ok(Self { all })
}
diff --git a/src/web.rs b/src/web.rs
index c287ce4..4bb4221 100644
--- a/src/web.rs
+++ b/src/web.rs
@@ -4,8 +4,8 @@ use axum::{
extract::{Query, State},
http::StatusCode,
response::{Html, IntoResponse, Response},
- routing::{get, post},
- Json, Router,
+ routing::get,
+ Router,
};
use color_eyre::{eyre::Context, Result};
use serde::{Deserialize, Serialize};
@@ -23,11 +23,8 @@ pub async fn webserver(db: Db) -> Result<()> {
.route("/", get(web_root))
.route("/build", get(web_build))
.route("/target", get(web_target))
- .route("/full-table", get(web_full_table))
+ .route("/nightly", get(web_nightly))
.route("/index.css", get(index_css))
- .route("/index.js", get(index_js))
- .route("/full-mega-monster", get(full_mega_monster))
- .route("/trigger-build", post(trigger_build))
.with_state(AppState { db });
info!("Serving website on port 3000 (commit {})", crate::VERSION);
@@ -144,24 +141,81 @@ async fn web_target(State(state): State, Query(query): Query) -> impl IntoResponse {
+#[derive(Deserialize)]
+struct NightlyQuery {
+ nightly: String,
+}
+
+async fn web_nightly(State(state): State, Query(query): Query) -> Response {
use askama::Template;
#[derive(askama::Template)]
- #[template(path = "index.html")]
- struct RootPage {
- targets: Vec,
+ #[template(path = "nightly.html")]
+ struct NightlyPage {
+ nightly: String,
version: &'static str,
+ builds: Vec<(String, Option, Option)>,
+ core_failures: usize,
+ std_failures: usize,
+ core_broken: Option,
+ std_broken: Option,
}
- match state.db.target_list().await {
- Ok(targets) => {
- let page = RootPage {
- targets,
- version: crate::VERSION,
- };
+ match state.db.history_for_nightly(&query.nightly).await {
+ Ok(builds) => match state.db.nightly_info(&query.nightly).await {
+ Ok(info) => {
+ let mut builds_grouped =
+ HashMap::, Option)>::new();
+ for build in &builds {
+ let v = builds_grouped.entry(build.target.clone()).or_default();
+ match build.mode {
+ BuildMode::Core => v.0 = Some(build.clone()),
+ BuildMode::MiriStd => v.1 = Some(build.clone()),
+ }
+ }
- Html(page.render().unwrap()).into_response()
- }
+ let mut std_failures = 0;
+ let mut core_failures = 0;
+ for build in builds {
+ if build.status == Status::Error {
+ match build.mode {
+ BuildMode::Core => core_failures += 1,
+ BuildMode::MiriStd => std_failures += 1,
+ }
+ }
+ }
+
+ let mut builds = builds_grouped
+ .into_iter()
+ .map(|(k, (v1, v2))| (k, v1, v2))
+ .collect::>();
+ builds.sort_by_cached_key(|build| build.0.clone());
+
+ let core_broken = info
+ .iter()
+ .find(|info| info.mode == BuildMode::Core && info.is_broken)
+ .and_then(|info| info.broken_error.clone());
+ let std_broken = info
+ .iter()
+ .find(|info| info.mode == BuildMode::MiriStd && info.is_broken)
+ .and_then(|info| info.broken_error.clone());
+
+ let page = NightlyPage {
+ nightly: query.nightly,
+ version: crate::VERSION,
+ builds,
+ std_failures,
+ core_failures,
+ core_broken,
+ std_broken,
+ };
+
+ Html(page.render().unwrap()).into_response()
+ }
+ Err(err) => {
+ error!(?err, "Error loading target state");
+ StatusCode::INTERNAL_SERVER_ERROR.into_response()
+ }
+ },
Err(err) => {
error!(?err, "Error loading target state");
StatusCode::INTERNAL_SERVER_ERROR.into_response()
@@ -169,9 +223,39 @@ async fn web_root(State(state): State) -> impl IntoResponse {
}
}
-async fn web_full_table() -> impl IntoResponse {
- Html(include_str!("../static/full-table.html").replace("{{version}}", crate::VERSION))
+async fn web_root(State(state): State) -> impl IntoResponse {
+ use askama::Template;
+ #[derive(askama::Template)]
+ #[template(path = "index.html")]
+ struct RootPage {
+ targets: Vec,
+ nightlies: Vec,
+ version: &'static str,
+ }
+
+ match state.db.target_list().await {
+ Ok(targets) => match state.db.nightly_list().await {
+ Ok(nightlies) => {
+ let page = RootPage {
+ targets,
+ nightlies,
+ version: crate::VERSION,
+ };
+
+ Html(page.render().unwrap()).into_response()
+ }
+ Err(err) => {
+ error!(?err, "Error loading nightly state");
+ StatusCode::INTERNAL_SERVER_ERROR.into_response()
+ }
+ },
+ Err(err) => {
+ error!(?err, "Error loading target state");
+ StatusCode::INTERNAL_SERVER_ERROR.into_response()
+ }
+ }
}
+
async fn index_css() -> impl IntoResponse {
(
[(
@@ -181,44 +265,12 @@ async fn index_css() -> impl IntoResponse {
include_str!("../static/index.css"),
)
}
-async fn index_js() -> impl IntoResponse {
- (
- [(
- axum::http::header::CONTENT_TYPE,
- axum::http::HeaderValue::from_static("text/javascript"),
- )],
- include_str!("../static/index.js"),
- )
-}
-
-async fn full_mega_monster(State(state): State) -> impl IntoResponse {
- state.db.full_mega_monster().await.map(Json).map_err(|err| {
- error!(?err, "Error loading target state");
- StatusCode::INTERNAL_SERVER_ERROR
- })
-}
#[derive(Serialize, Deserialize)]
struct TriggerBuildBody {
nightly: String,
}
-#[axum::debug_handler]
-async fn trigger_build(
- State(_state): State,
- _body: Json,
-) -> StatusCode {
- return StatusCode::BAD_REQUEST;
- // tokio::spawn(async move {
- // let result = build::build_every_target_for_toolchain(&state.db, &body.nightly).await;
- // if let Err(err) = result {
- // error!(?err, "Error while building");
- // }
- // });
- //
- // StatusCode::ACCEPTED
-}
-
impl Status {
fn to_emoji(&self) -> &'static str {
match self {
diff --git a/static/build.html b/static/build.html
index 31af71d..db0ada4 100644
--- a/static/build.html
+++ b/static/build.html
@@ -1,4 +1,4 @@
-
+
@@ -19,6 +19,12 @@
{{stderr}}
+
+ Build history for target {{target}}
+
+
+ Build state for nightly {{nightly}}
+
- This checks that codegen/linking of core works, but does not check whether std builds.
- Target Filter
-
- Filter failed
-
-
-
-
-
- Std Check Build
- Builds every target with:
-
cargo miri setup
- This checks that std builds (on targets that have it) but does not check whether codegen/linking works.
- Target Filter
-
- Filter failed
-
-
-
-
-
-
-
-
diff --git a/static/index.js b/static/index.js
deleted file mode 100644
index 0828dfc..0000000
--- a/static/index.js
+++ /dev/null
@@ -1,159 +0,0 @@
-class Table {
- constructor(data, tableElemId, filterElemId, filterFailedElemId) {
- this.data = data;
- this.elem = document.getElementById(tableElemId);
-
- document.getElementById(filterElemId).addEventListener("input", (e) => {
- this.filter.search = e.target.value;
- this.render();
- });
- document
- .getElementById(filterFailedElemId)
- .addEventListener("input", (e) => {
- this.filter.filterFailed = e.target.checked;
- this.render();
- });
-
- this.filter = {
- search: "",
- filterFailed: false,
- };
- }
-
- update(data) {
- this.data = data;
- }
-
- render() {
- const allTargets = new Set();
- const allNightlies = new Set();
-
- // The infos grouped by target.
- const targetInfos = new Map();
-
- // Targets that have, at some point, errored
- const targetsWithErrors = new Set();
-
- // Whether a nightly is completely broken.
- // These are still filtered out when filter failed is selected.
- const isNightlyBroken = new Map();
-
- // The first pass over the data, to find nightlies that are broken.
- for (const info of this.data) {
- if (!isNightlyBroken.has(info.nightly)) {
- // Assume that a nightly is broken until proven otherwise.
- isNightlyBroken.set(info.nightly, true);
- }
- if (info.status == "pass") {
- // This nightly has built something, so it's clearly not broken :).
- isNightlyBroken.set(info.nightly, false);
- }
- }
-
- // Second pass over the data, group by nightly and prepare data for filter.
- for (const info of this.data) {
- allNightlies.add(info.nightly);
-
- if (!info.target.includes(this.filter.search)) {
- continue;
- }
-
- if (info.status === "error" && !isNightlyBroken.get(info.nightly)) {
- targetsWithErrors.add(info.target);
- }
-
- allTargets.add(info.target);
- if (!targetInfos.has(info.target)) {
- targetInfos.set(info.target, new Map());
- }
- targetInfos.get(info.target).set(info.nightly, info);
- }
-
- const nightlies = Array.from(allNightlies);
- nightlies.sort();
- nightlies.reverse();
- const targets = Array.from(allTargets);
- targets.sort();
-
- const header = document.createElement("tr");
- const headerTarget = document.createElement("th");
- headerTarget.innerText = "target";
- header.appendChild(headerTarget);
- nightlies.forEach((target) => {
- const elem = document.createElement("th");
- elem.classList.add("target-header");
- elem.innerText = target;
- header.appendChild(elem);
- });
-
- const rows = targets.flatMap((target) => {
- if (this.filter.filterFailed && !targetsWithErrors.has(target)) {
- return [];
- }
-
- const tr = document.createElement("tr");
-
- const targetCol = document.createElement("td");
- targetCol.innerText = target;
- targetCol.classList.add("target-name-col");
- tr.appendChild(targetCol);
-
- const info = targetInfos.get(target) ?? new Map();
-
- for (const nightly of nightlies) {
- const td = document.createElement("td");
- const targetInfo = info.get(nightly);
-
- if (targetInfo) {
- const a = document.createElement("a");
- a.classList.add("build-info-a");
- a.href = `build?nightly=${encodeURIComponent(
- nightly
- )}&target=${encodeURIComponent(target)}&mode=${encodeURIComponent(
- targetInfo.mode
- )}`;
- a.innerText = targetInfo.status == "pass" ? "✅" : "❌";
- td.appendChild(a);
- td.classList.add("build-cell");
- td.classList.add(targetInfo.status);
- } else {
- td.innerText = "";
- td.classList.add("missing");
- }
- tr.appendChild(td);
- }
-
- return [tr];
- });
- this.elem.replaceChildren(header, ...rows);
- }
-}
-
-const coreTable = new Table(
- [],
- "target-state",
- "target-filter",
- "target-filter-failed"
-);
-const miriTable = new Table(
- [],
- "target-state-miri",
- "target-filter-miri",
- "target-filter-failed-miri"
-);
-
-function fetchTargets() {
- fetch("full-mega-monster")
- .then((body) => body.json())
- .then((body) => {
- const core = body.filter((info) => info.mode === "core");
- const miri = body.filter((info) => info.mode === "miri-std");
- coreTable.update(core);
- miriTable.update(miri);
- coreTable.render();
- miriTable.render();
- });
-}
-
-// Initial fetch
-fetchTargets();
diff --git a/templates/index.html b/templates/index.html
index 4177386..421a894 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -1,4 +1,4 @@
-
+
@@ -22,7 +22,22 @@
your browser).
-
+ Nightlies
+
+ {% for nightly in nightlies.iter().take(5) %}
+
+ {{ nightly }}
+
+ {% endfor %}
+
+
+ To view a list of all nightlies, check
+ the list at the end .
+
+
+ Targets
+
+
{% for target in targets %}
{{ target }}
@@ -30,6 +45,18 @@
{% endfor %}
+
+ All Nightlies
+
+
+ {% for nightly in nightlies %}
+
+ {{ nightly }}
+
+ {% endfor %}
+
+
+
-
diff --git a/templates/nightly.html b/templates/nightly.html
new file mode 100644
index 0000000..e4169e4
--- /dev/null
+++ b/templates/nightly.html
@@ -0,0 +1,97 @@
+
+
+
+
+
+ {{nightly}} build history
+
+
+
+ Nightly build state for {{nightly}}
+ Back
+
+ This contains the status of this nightly. Core is built with
+ cargo build --release -Zbuild-std=core. This checks that
+ codegen/linking of core works, but does not check whether std builds.
+
+
+ std is being built with cargo miri setup. If a target does
+ not support std, the std column represents core/alloc. This checks that
+ std builds (on targets that have it) but does not check whether
+ codegen/linking works.
+
+ {% if let Some(core_broken) = core_broken %}
+
+ ⚠️ The core build is broken in general for this nightly, so no data is available ⚠️
+
+
+
+ {{core_broken}}
+
+ {% endif %}
+ {% if let Some(std_broken) = std_broken %}
+
+ ⚠️ The std build is broken for this nightly, so no data is available ⚠️
+
+
+
+ {{std_broken}}
+
+ {% endif %}
+
+
+
+ core failures
+ {{core_failures}}
+ std failures
+ {{std_failures}}
+
+
+
+
+
+
diff --git a/templates/target.html b/templates/target.html
index 5c2135e..af2ba20 100644
--- a/templates/target.html
+++ b/templates/target.html
@@ -36,7 +36,7 @@
{% for build in builds %}
- {{ build.0 }}
+ {{ build.0 }}
{% match build.1 %} {% when Some with (build) %}