diff --git a/migrations/20250703173258_index.sql b/migrations/20250703173258_index.sql
deleted file mode 100644
index ee4693a..0000000
--- a/migrations/20250703173258_index.sql
+++ /dev/null
@@ -1,5 +0,0 @@
--- 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
deleted file mode 100644
index 8f6dfde..0000000
--- a/migrations/20250703200547_broken-error.sql
+++ /dev/null
@@ -1,4 +0,0 @@
--- 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 32fa1e1..9fcb5b3 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, &format!("{err:?}"))
+ db.finish_nightly_as_broken(&nightly, mode)
.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(60 * 60)).await;
+ tokio::time::sleep(Duration::from_secs(1 * 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 42c1811..75a87a4 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -33,7 +33,7 @@ impl Display for BuildMode {
}
}
-#[derive(sqlx::FromRow, Serialize, Deserialize, Clone)]
+#[derive(sqlx::FromRow, Serialize, Deserialize)]
pub struct BuildInfo {
pub nightly: String,
pub target: String,
@@ -73,14 +73,6 @@ 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)
@@ -89,7 +81,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 })
}
@@ -108,6 +100,13 @@ 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 = ?",
@@ -118,26 +117,6 @@ 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 {
@@ -151,21 +130,6 @@ 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,
@@ -222,16 +186,10 @@ impl Db {
Ok(())
}
- 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, ?)")
+ 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)")
.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 f6675dd..7493870 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 = "2022-01-01";
+const EARLIEST_CUTOFF_DATE: &str = "2023-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,10 +54,7 @@ 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 4bb4221..c287ce4 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,
- Router,
+ routing::{get, post},
+ Json, Router,
};
use color_eyre::{eyre::Context, Result};
use serde::{Deserialize, Serialize};
@@ -23,8 +23,11 @@ pub async fn webserver(db: Db) -> Result<()> {
.route("/", get(web_root))
.route("/build", get(web_build))
.route("/target", get(web_target))
- .route("/nightly", get(web_nightly))
+ .route("/full-table", get(web_full_table))
.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);
@@ -141,114 +144,24 @@ async fn web_target(State(state): State, Query(query): Query, Query(query): Query) -> Response {
- use askama::Template;
- #[derive(askama::Template)]
- #[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.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()),
- }
- }
-
- 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()
- }
- }
-}
-
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,
- };
+ Ok(targets) => {
+ let page = RootPage {
+ targets,
+ version: crate::VERSION,
+ };
- Html(page.render().unwrap()).into_response()
- }
- Err(err) => {
- error!(?err, "Error loading nightly state");
- StatusCode::INTERNAL_SERVER_ERROR.into_response()
- }
- },
+ Html(page.render().unwrap()).into_response()
+ }
Err(err) => {
error!(?err, "Error loading target state");
StatusCode::INTERNAL_SERVER_ERROR.into_response()
@@ -256,6 +169,9 @@ 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 index_css() -> impl IntoResponse {
(
[(
@@ -265,12 +181,44 @@ 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 db0ada4..31af71d 100644
--- a/static/build.html
+++ b/static/build.html
@@ -1,4 +1,4 @@
-
+
@@ -19,12 +19,6 @@
{{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
new file mode 100644
index 0000000..0828dfc
--- /dev/null
+++ b/static/index.js
@@ -0,0 +1,159 @@
+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 421a894..4177386 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -1,4 +1,4 @@
-
+
@@ -22,22 +22,7 @@
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 }}
@@ -45,18 +30,6 @@
{% endfor %}
-
- All Nightlies
-
-
- {% for nightly in nightlies %}
-
- {{ nightly }}
-
- {% endfor %}
-
-
-
+
diff --git a/templates/nightly.html b/templates/nightly.html
deleted file mode 100644
index e4169e4..0000000
--- a/templates/nightly.html
+++ /dev/null
@@ -1,97 +0,0 @@
-
-
-
-
-
- {{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 af2ba20..5c2135e 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) %}