Add nightly overview pages

This commit is contained in:
nora 2025-07-03 19:26:02 +02:00
parent 7f4c69e51f
commit 61d78680e0
9 changed files with 229 additions and 41 deletions

View file

@ -57,7 +57,7 @@ async fn background_builder_inner(db: &Db, nightly_cache: &mut NightlyCache) ->
match next { match next {
Some((nightly, mode)) => { Some((nightly, mode)) => {
info!(%nightly, %mode, "Building next nightly"); 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 .await
.wrap_err_with(|| format!("building targets for toolchain {nightly}")); .wrap_err_with(|| format!("building targets for toolchain {nightly}"));
if let Err(err) = result { if let Err(err) = result {
@ -69,7 +69,7 @@ async fn background_builder_inner(db: &Db, nightly_cache: &mut NightlyCache) ->
} }
None => { None => {
info!("No new nightly, waiting for an hour to try again"); 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(()) Ok(())
@ -191,7 +191,7 @@ pub async fn build_every_target_for_toolchain(
let results = futures::stream::iter( let results = futures::stream::iter(
targets targets
.iter() .iter()
.map(|target| build_single_target(&db, nightly, target, mode)), .map(|target| build_single_target(db, nightly, target, mode)),
) )
.buffer_unordered(concurrent) .buffer_unordered(concurrent)
.collect::<Vec<Result<()>>>() .collect::<Vec<Result<()>>>()
@ -266,7 +266,7 @@ async fn build_target(
BuildMode::Core => { BuildMode::Core => {
let init = Command::new("cargo") let init = Command::new("cargo")
.args(["init", "--lib", "--name", "target-test"]) .args(["init", "--lib", "--name", "target-test"])
.current_dir(&tmpdir) .current_dir(tmpdir)
.output() .output()
.await .await
.wrap_err("spawning cargo init")?; .wrap_err("spawning cargo init")?;
@ -282,7 +282,7 @@ async fn build_target(
.arg(format!("+{toolchain}")) .arg(format!("+{toolchain}"))
.args(["build", "-Zbuild-std=core", "--release"]) .args(["build", "-Zbuild-std=core", "--release"])
.args(["--target", target]) .args(["--target", target])
.current_dir(&tmpdir) .current_dir(tmpdir)
.output() .output()
.await .await
.wrap_err("spawning cargo build")? .wrap_err("spawning cargo build")?
@ -291,7 +291,7 @@ async fn build_target(
.arg(format!("+{toolchain}")) .arg(format!("+{toolchain}"))
.args(["miri", "setup"]) .args(["miri", "setup"])
.args(["--target", target]) .args(["--target", target])
.current_dir(&tmpdir) .current_dir(tmpdir)
.env("MIRI_SYSROOT", tmpdir) .env("MIRI_SYSROOT", tmpdir)
.output() .output()
.await .await

View file

@ -33,7 +33,7 @@ impl Display for BuildMode {
} }
} }
#[derive(sqlx::FromRow, Serialize, Deserialize)] #[derive(sqlx::FromRow, Serialize, Deserialize, Clone)]
pub struct BuildInfo { pub struct BuildInfo {
pub nightly: String, pub nightly: String,
pub target: String, pub target: String,
@ -81,7 +81,7 @@ impl Db {
let conn = Pool::connect_with(db_opts) let conn = Pool::connect_with(db_opts)
.await .await
.wrap_err_with(|| format!("opening db from `{}`", path))?; .wrap_err_with(|| format!("opening db from `{path}`"))?;
Ok(Self { conn }) Ok(Self { conn })
} }
@ -117,6 +117,16 @@ impl Db {
.wrap_err("getting history for single target") .wrap_err("getting history for single target")
} }
pub async fn history_for_nightly(&self, nightly: &str) -> Result<Vec<BuildInfo>> {
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 target_list(&self) -> Result<Vec<String>> { pub async fn target_list(&self) -> Result<Vec<String>> {
#[derive(sqlx::FromRow)] #[derive(sqlx::FromRow)]
struct TargetName { struct TargetName {
@ -130,6 +140,21 @@ impl Db {
.map(|elems| elems.into_iter().map(|elem| elem.target).collect()) .map(|elems| elems.into_iter().map(|elem| elem.target).collect())
} }
pub async fn nightly_list(&self) -> Result<Vec<String>> {
#[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( pub async fn build_status_full(
&self, &self,
nightly: &str, nightly: &str,

View file

@ -43,7 +43,7 @@ impl Nightlies {
.last() .last()
.ok_or_eyre("did not find any nightlies in manifests.txt")?; .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) if nightly_exists(&nightly, cache)
.await .await
.wrap_err_with(|| format!("checking whether {nightly} exists"))? .wrap_err_with(|| format!("checking whether {nightly} exists"))?
@ -54,7 +54,10 @@ impl Nightlies {
all.reverse(); 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 }) Ok(Self { all })
} }

View file

@ -4,7 +4,7 @@ use axum::{
extract::{Query, State}, extract::{Query, State},
http::StatusCode, http::StatusCode,
response::{Html, IntoResponse, Response}, response::{Html, IntoResponse, Response},
routing::{get, post}, routing::get,
Json, Router, Json, Router,
}; };
use color_eyre::{eyre::Context, Result}; use color_eyre::{eyre::Context, Result};
@ -23,11 +23,11 @@ pub async fn webserver(db: Db) -> Result<()> {
.route("/", get(web_root)) .route("/", get(web_root))
.route("/build", get(web_build)) .route("/build", get(web_build))
.route("/target", get(web_target)) .route("/target", get(web_target))
.route("/nightly", get(web_nightly))
.route("/full-table", get(web_full_table)) .route("/full-table", get(web_full_table))
.route("/index.css", get(index_css)) .route("/index.css", get(index_css))
.route("/index.js", get(index_js)) .route("/index.js", get(index_js))
.route("/full-mega-monster", get(full_mega_monster)) .route("/full-mega-monster", get(full_mega_monster))
.route("/trigger-build", post(trigger_build))
.with_state(AppState { db }); .with_state(AppState { db });
info!("Serving website on port 3000 (commit {})", crate::VERSION); info!("Serving website on port 3000 (commit {})", crate::VERSION);
@ -144,24 +144,95 @@ async fn web_target(State(state): State<AppState>, Query(query): Query<TargetQue
} }
} }
#[derive(Deserialize)]
struct NightlyQuery {
nightly: String,
}
async fn web_nightly(State(state): State<AppState>, Query(query): Query<NightlyQuery>) -> Response {
use askama::Template;
#[derive(askama::Template)]
#[template(path = "nightly.html")]
struct NightlyPage {
nightly: String,
version: &'static str,
builds: Vec<(String, Option<BuildInfo>, Option<BuildInfo>)>,
core_failures: usize,
std_failures: usize,
}
match state.db.history_for_nightly(&query.nightly).await {
Ok(builds) => {
let mut builds_grouped =
HashMap::<String, (Option<BuildInfo>, Option<BuildInfo>)>::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::<Vec<_>>();
builds.sort_by_cached_key(|build| build.0.clone());
let page = NightlyPage {
nightly: query.nightly,
version: crate::VERSION,
builds,
std_failures,
core_failures,
};
Html(page.render().unwrap()).into_response()
}
Err(err) => {
error!(?err, "Error loading target state");
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
}
}
async fn web_root(State(state): State<AppState>) -> impl IntoResponse { async fn web_root(State(state): State<AppState>) -> impl IntoResponse {
use askama::Template; use askama::Template;
#[derive(askama::Template)] #[derive(askama::Template)]
#[template(path = "index.html")] #[template(path = "index.html")]
struct RootPage { struct RootPage {
targets: Vec<String>, targets: Vec<String>,
nightlies: Vec<String>,
version: &'static str, version: &'static str,
} }
match state.db.target_list().await { match state.db.target_list().await {
Ok(targets) => { Ok(targets) => match state.db.nightly_list().await {
let page = RootPage { Ok(nightlies) => {
targets, let page = RootPage {
version: crate::VERSION, targets,
}; nightlies,
version: crate::VERSION,
};
Html(page.render().unwrap()).into_response() Html(page.render().unwrap()).into_response()
} }
Err(err) => {
error!(?err, "Error loading nightly state");
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
},
Err(err) => { Err(err) => {
error!(?err, "Error loading target state"); error!(?err, "Error loading target state");
StatusCode::INTERNAL_SERVER_ERROR.into_response() StatusCode::INTERNAL_SERVER_ERROR.into_response()
@ -203,22 +274,6 @@ struct TriggerBuildBody {
nightly: String, nightly: String,
} }
#[axum::debug_handler]
async fn trigger_build(
State(_state): State<AppState>,
_body: Json<TriggerBuildBody>,
) -> 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 { impl Status {
fn to_emoji(&self) -> &'static str { fn to_emoji(&self) -> &'static str {
match self { match self {

View file

@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
@ -19,6 +19,12 @@
<pre> <pre>
{{stderr}} {{stderr}}
</pre> </pre>
<p>
<a href="/target?target={{target}}">Build history for target {{target}}</a>
</p>
<p>
<a href="/nightly?nightly={{nightly}}">Build state for nightly {{nightly}}</a>
</p>
<footer class="footer"> <footer class="footer">
<span>does-it-build {{version}}</span> <span>does-it-build {{version}}</span>
<a href="https://github.com/Noratrieb/does-it-build"> <a href="https://github.com/Noratrieb/does-it-build">

View file

@ -3,13 +3,13 @@ class Table {
this.data = data; this.data = data;
this.elem = document.getElementById(tableElemId); this.elem = document.getElementById(tableElemId);
document.getElementById(filterElemId).addEventListener("input", (e) => { document.getElementById(filterElemId)?.addEventListener("input", (e) => {
this.filter.search = e.target.value; this.filter.search = e.target.value;
this.render(); this.render();
}); });
document document
.getElementById(filterFailedElemId) .getElementById(filterFailedElemId)
.addEventListener("input", (e) => { ?.addEventListener("input", (e) => {
this.filter.filterFailed = e.target.checked; this.filter.filterFailed = e.target.checked;
this.render(); this.render();
}); });

View file

@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
@ -22,7 +22,16 @@
your browser). your browser).
</p> </p>
<ul class="target-list"> <h1>Nightlies</h1>
<ul></ul>
<p>
To view a list of all nightlies, check
<a href="#all-nightlies">the list at the end</a>.
</p>
<h2>Targets</h2>
<ul>
{% for target in targets %} {% for target in targets %}
<li> <li>
<a href="target?target={{ target }}">{{ target }}</a> <a href="target?target={{ target }}">{{ target }}</a>
@ -30,6 +39,18 @@
{% endfor %} {% endfor %}
</ul> </ul>
<section id="all-nightlies">
<h2>All Nightlies</h2>
<ul>
{% for nightly in nightlies %}
<li>
<a href="nightly?nightly={{ nightly }}">{{ nightly }}</a>
</li>
{% endfor %}
</ul>
</section>
<footer class="footer"> <footer class="footer">
<span>does-it-build {{version}}</span> <span>does-it-build {{version}}</span>
<a href="https://github.com/Noratrieb/does-it-build"> <a href="https://github.com/Noratrieb/does-it-build">

78
templates/nightly.html Normal file
View file

@ -0,0 +1,78 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{nightly}} build history</title>
<link rel="stylesheet" href="/index.css" />
</head>
<body>
<h1>Nightly build state for {{nightly}}</h1>
<a href="/">Back</a>
<p>
This contains the status of this nightly. Core is built with
<code>cargo build --release -Zbuild-std=core</code>. This checks that
codegen/linking of core works, but does not check whether std builds.
</p>
<p>
std is being built with <code>cargo miri setup</code>. 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.
</p>
<p>
<dl>
<dt>core failures</dt>
<dd>{{core_failures}}</dd>
<dt>std failures</dt>
<dd>{{std_failures}}</dd>
</dl>
</p>
<table>
<tr>
<th>nightly</th>
<th>core</th>
<th>std</th>
</tr>
{% for build in builds %}
<tr>
<td><a href="/target?target={{build.0}}">{{ build.0 }}</a></td>
{% match build.1 %} {% when Some with (build) %}
<td class="build-cell">
<a class="build-info-a" href="{{ build.link() }}">
{{ build.status.to_emoji() }}
</a>
</td>
{% when None %}
<td class="missing"></td>
{% endmatch %} {% match build.2 %} {% when Some with (build) %}
<td class="build-cell">
<a class="build-info-a" href="{{ build.link() }}">
<span> {{ build.status.to_emoji() }} </span>
</a>
</td>
{% when None %}
<td class="missing"></td>
{% endmatch %}
</tr>
{% endfor %}
</table>
<footer class="footer">
<span>does-it-build {{version}}</span>
<a href="https://github.com/Noratrieb/does-it-build">
<svg
viewBox="0 0 16 16"
width="32"
height="32"
aria-labelledby="github-logo-title"
>
<title id="github-logo-title">GitHub</title>
<path
fill="black"
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
></path>
</svg>
</a>
</footer>
</body>
</html>

View file

@ -36,7 +36,7 @@
</tr> </tr>
{% for build in builds %} {% for build in builds %}
<tr> <tr>
<td>{{ build.0 }}</td> <td><a href="/nightly?nightly={{build.0}}">{{ build.0 }}</a></td>
{% match build.1 %} {% when Some with (build) %} {% match build.1 %} {% when Some with (build) %}
<td class="build-cell"> <td class="build-cell">
<a class="build-info-a" href="{{ build.link() }}"> <a class="build-info-a" href="{{ build.link() }}">