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}} +