Compare commits

...

4 commits

Author SHA1 Message Date
5fb3ca86b2 Support suspending background thread
Useful for locally testing query timings with sqlx debug logging.
2025-07-04 23:09:52 +02:00
6df7155a50 Add index for build stats
Otherwise it's very slow :)
2025-07-04 23:04:51 +02:00
7f1702bc28 Store more metadata
And other various improvements
2025-07-04 22:47:06 +02:00
f6fac25c6f Remove old link and show number of builds
In the users locale of course. Without any annoying Accept-Language
parsing or anything like that, just with the beauty of JavaScript.
2025-07-04 22:06:40 +02:00
12 changed files with 191 additions and 100 deletions

View file

@ -12,6 +12,12 @@ fn main() {
};
println!("cargo:rustc-env=GIT_COMMIT={version}");
let version_short = if version.len() > 16 {
&version[..16]
} else {
&version
};
println!("cargo:rustc-env=GIT_COMMIT_SHORT={version_short}");
}
fn try_get_commit() -> color_eyre::Result<String> {

View file

@ -0,0 +1,10 @@
-- Add migration script here
ALTER TABLE "build_info"
ADD COLUMN "build_date" INTEGER NULL;
ALTER TABLE "build_info"
ADD COLUMN "build_duration_ms" INTEGER NULL;
ALTER TABLE "build_info"
ADD COLUMN "does_it_build_version" VARCHAR NULL;

View file

@ -0,0 +1,3 @@
-- Add migration script here
CREATE INDEX IF NOT EXISTS build_info_status ON build_info (status);

View file

@ -2,7 +2,7 @@ use std::{
fmt::{Debug, Display},
num::NonZeroUsize,
path::Path,
time::Duration,
time::{Duration, Instant, SystemTime},
};
use color_eyre::{
@ -52,6 +52,13 @@ impl Display for Toolchain {
}
pub async fn background_builder(db: Db) -> Result<()> {
if concurrent_jobs() == 0 {
info!("Suspending background thread since DOES_IT_BUILD_PARALLEL_JOBS=0");
loop {
tokio::time::sleep(Duration::from_secs(3600)).await;
}
}
loop {
if let Err(err) = background_builder_inner(&db).await {
error!("error in background builder: {err}");
@ -192,21 +199,12 @@ pub async fn build_every_target_for_toolchain(
.await
.wrap_err("failed to get targets")?;
let concurrent = std::env::var("DOES_IT_BUILD_PARALLEL_JOBS")
.map(|jobs| jobs.parse().unwrap())
.unwrap_or_else(|_| {
std::thread::available_parallelism()
.unwrap_or(NonZeroUsize::new(2).unwrap())
.get()
/ 2
});
let results = futures::stream::iter(
targets
.iter()
.map(|target| build_single_target(db, nightly, target, mode)),
)
.buffer_unordered(concurrent)
.buffer_unordered(concurrent_jobs())
.collect::<Vec<Result<()>>>()
.await;
for result in results {
@ -242,6 +240,8 @@ async fn build_single_target(db: &Db, nightly: &str, target: &str, mode: BuildMo
let tmpdir = tempfile::tempdir().wrap_err("creating temporary directory")?;
let start_time = Instant::now();
let result = build_target(
tmpdir.path(),
&Toolchain::from_nightly(nightly),
@ -258,6 +258,16 @@ async fn build_single_target(db: &Db, nightly: &str, target: &str, mode: BuildMo
stderr: result.stderr,
mode,
rustflags: result.rustflags,
build_date: Some(
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_millis()
.try_into()
.unwrap(),
),
does_it_build_version: Some(crate::VERSION_SHORT.into()),
build_duration_ms: Some(start_time.elapsed().as_millis().try_into().unwrap()),
})
.await?;
@ -342,3 +352,14 @@ async fn build_target(
rustflags,
})
}
fn concurrent_jobs() -> usize {
std::env::var("DOES_IT_BUILD_PARALLEL_JOBS")
.map(|jobs| jobs.parse().unwrap())
.unwrap_or_else(|_| {
std::thread::available_parallelism()
.unwrap_or(NonZeroUsize::new(2).unwrap())
.get()
/ 2
})
}

View file

@ -49,6 +49,9 @@ pub struct FullBuildInfo {
pub stderr: String,
pub mode: BuildMode,
pub rustflags: Option<String>,
pub build_date: Option<i64>,
pub does_it_build_version: Option<String>,
pub build_duration_ms: Option<i64>,
}
#[derive(Debug, PartialEq, Clone, Copy, sqlx::Type, Serialize, Deserialize)]
@ -82,6 +85,17 @@ pub struct FinishedNightlyWithBroken {
pub broken_error: Option<String>,
}
pub struct BuildStats {
pub pass_count: u32,
pub error_count: u32,
}
impl BuildStats {
pub fn total(&self) -> u32 {
self.pass_count + self.error_count
}
}
impl Db {
pub async fn open(path: &str) -> Result<Self> {
let db_opts = SqliteConnectOptions::from_str(path)
@ -96,7 +110,8 @@ impl Db {
pub async fn insert(&self, info: FullBuildInfo) -> Result<()> {
sqlx::query(
"INSERT INTO build_info (nightly, target, status, stderr, mode, rustflags) VALUES (?, ?, ?, ?, ?, ?);",
"INSERT INTO build_info (nightly, target, status, stderr, mode, rustflags, build_date, does_it_build_version, build_duration_ms) \
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);",
)
.bind(info.nightly)
.bind(info.target)
@ -104,6 +119,9 @@ impl Db {
.bind(info.stderr)
.bind(info.mode)
.bind(info.rustflags)
.bind(info.build_date)
.bind(info.does_it_build_version)
.bind(info.build_duration_ms)
.execute(&self.conn)
.await
.wrap_err("inserting build info into database")?;
@ -168,6 +186,34 @@ impl Db {
.map(|elems| elems.into_iter().map(|elem| elem.nightly).collect())
}
pub async fn build_count(&self) -> Result<BuildStats> {
#[derive(sqlx::FromRow)]
struct BuildStat {
build_count: u32,
status: Status,
}
let results = sqlx::query_as::<_, BuildStat>(
"SELECT COUNT(status) as build_count, status FROM build_info GROUP BY status",
)
.fetch_all(&self.conn)
.await
.wrap_err("getting list of all targets")?;
let count = |status| {
results
.iter()
.find(|row| row.status == status)
.map(|row| row.build_count)
.unwrap_or(0)
};
Ok(BuildStats {
pass_count: count(Status::Pass),
error_count: count(Status::Error),
})
}
pub async fn build_status_full(
&self,
nightly: &str,
@ -175,7 +221,7 @@ impl Db {
mode: BuildMode,
) -> Result<Option<FullBuildInfo>> {
let result = sqlx::query_as::<_, FullBuildInfo>(
"SELECT nightly, target, status, stderr, mode, rustflags FROM build_info
"SELECT nightly, target, status, stderr, mode, rustflags, build_date, does_it_build_version, build_duration_ms FROM build_info
WHERE nightly = ? AND target = ? AND mode = ?",
)
.bind(nightly)

View file

@ -8,6 +8,7 @@ use db::Db;
use tracing_subscriber::EnvFilter;
const VERSION: &str = env!("GIT_COMMIT");
const VERSION_SHORT: &str = env!("GIT_COMMIT_SHORT");
#[tokio::main]
async fn main() -> Result<()> {

View file

@ -11,7 +11,7 @@ use color_eyre::{eyre::Context, Result};
use serde::{Deserialize, Serialize};
use tracing::{error, info};
use crate::db::{BuildInfo, BuildMode, Db, Status};
use crate::db::{BuildInfo, BuildMode, BuildStats, Db, Status};
#[derive(Clone)]
pub struct AppState {
@ -25,6 +25,7 @@ pub async fn webserver(db: Db) -> Result<()> {
.route("/target", get(web_target))
.route("/nightly", get(web_nightly))
.route("/index.css", get(index_css))
.route("/index.js", get(index_js))
.with_state(AppState { db });
info!("Serving website on port 3000 (commit {})", crate::VERSION);
@ -52,6 +53,9 @@ async fn web_build(State(state): State<AppState>, Query(query): Query<BuildQuery
rustflags: Option<String>,
version: &'static str,
status: Status,
build_date: Option<String>,
build_duration_s: Option<f32>,
does_it_build_version: Option<String>,
}
match state
@ -72,6 +76,19 @@ async fn web_build(State(state): State<AppState>, Query(query): Query<BuildQuery
rustflags: build.rustflags,
version: crate::VERSION,
status: build.status,
build_date: build.build_date.map(|build_date| {
time::OffsetDateTime::from_unix_timestamp_nanos(build_date as i128 * 1000000)
.map(|build_date| {
build_date
.format(&time::format_description::well_known::Rfc3339)
.unwrap()
})
.unwrap()
}),
build_duration_s: build
.build_duration_ms
.map(|build_duration_ms| (build_duration_ms as f32) / 1000.0),
does_it_build_version: build.does_it_build_version,
};
Html(page.render().unwrap()).into_response()
}
@ -257,35 +274,37 @@ async fn web_nightly(State(state): State<AppState>, Query(query): Query<NightlyQ
async fn web_root(State(state): State<AppState>) -> impl IntoResponse {
use askama::Template;
#[derive(askama::Template)]
#[template(path = "index.html")]
struct RootPage {
targets: Vec<String>,
nightlies: Vec<String>,
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 render(state: AppState) -> Result<Response> {
#[derive(askama::Template)]
#[template(path = "index.html")]
struct RootPage {
targets: Vec<String>,
nightlies: Vec<String>,
version: &'static str,
build_count: BuildStats,
}
let targets = state.db.target_list().await?;
let nightlies = state.db.nightly_list().await?;
let build_count = state.db.build_count().await?;
let page = RootPage {
targets,
nightlies,
version: crate::VERSION,
build_count,
};
Ok(Html(page.render().unwrap()).into_response())
}
render(state)
.await
.unwrap_or_else(|err: color_eyre::eyre::Error| {
error!(?err, "Error loading data for root page");
StatusCode::INTERNAL_SERVER_ERROR.into_response()
})
}
async fn index_css() -> impl IntoResponse {
@ -298,6 +317,16 @@ async fn index_css() -> impl IntoResponse {
)
}
async fn index_js() -> impl IntoResponse {
(
[(
axum::http::header::CONTENT_TYPE,
axum::http::HeaderValue::from_static("application/javascript; charset=utf-8"),
)],
include_str!("../static/index.js"),
)
}
#[derive(Serialize, Deserialize)]
struct TriggerBuildBody {
nightly: String,

13
static/index.js Normal file
View file

@ -0,0 +1,13 @@
// Do some fancy number formatting in your locale, so you can't blame me if the numbers look ass.
document.querySelectorAll(".number").forEach((elem) => {
elem.textContent = new Intl.NumberFormat().format(Number(elem.textContent));
});
// Do some fancy date formatting in your locale, so you can't blame me if the numbers look ass.
document.querySelectorAll(".date").forEach((elem) => {
console.log("run")
elem.textContent = new Intl.DateTimeFormat(undefined, {
dateStyle: "short",
timeStyle: "short",
}).format(new Date(elem.textContent));
});

View file

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Build {{nightly}} {{target}}</title>
<link rel="stylesheet" href="/index.css" />
<script type="module" defer src="index.js"></script>
<style>
.build-indicator-big {
padding: 10px;
@ -21,6 +22,12 @@
<p>
Using rustflags: <b><code>{{rustflags}}</code></b>
</p>
{% endif %} {% if let Some(build_date) = build_date %}
<p>Build date: <span class="date">{{build_date}}</span></p>
{% endif %} {% if let Some(build_duration_s) = build_duration_s %}
<p>Build duration: <span class="number">{{build_duration_s}}</span>s</p>
{% endif %} {% if let Some(does_it_build_version) = does_it_build_version %}
<p>Builder does-it-build commit: <span>{{does_it_build_version}}</span></p>
{% endif %}
<pre>
{{stderr}}
@ -36,20 +43,8 @@
>
</p>
<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>
does-it-build {{version}}
</a>
</footer>
</body>

View file

@ -5,21 +5,24 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Does it build?</title>
<link rel="stylesheet" href="index.css" />
<style></style>
<script type="module" defer src="index.js"></script>
</head>
<body>
<h1>Does it build?</h1>
<p>
This website builds every rustc target on many nightlies to check which
ones work and which ones do not.
This website builds the standard libary for every rustc target on every
nightly to check whether it builds or not.
</p>
<p>
There are currently
<span class="number">{{build_count.total()}}</span> build results stored,
with <span class="number">{{build_count.pass_count}}</span> passing and
<span class="number">{{build_count.error_count}}</span> erroring.
</p>
<p>You can select a target from the list below and check its history.</p>
<p>
If that is not what you want, you can visit the
<a href="full-table">full table</a> to see
<strong>all</strong> information at once (which will really stress test
your browser).
You can select a nightly or target from the list below and check its
history.
</p>
<h1>Nightlies</h1>
@ -58,20 +61,8 @@
</section>
<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>
does-it-build {{version}}
</a>
</footer>
</body>

View file

@ -86,20 +86,8 @@
{% 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>
does-it-build {{version}}
</a>
</footer>
</body>

View file

@ -67,20 +67,8 @@
{% 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>
does-it-build {{version}}
</a>
</footer>
</body>