Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1419309a2d |
Generated
+187
-8
@@ -2,6 +2,12 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
@@ -84,6 +90,12 @@ version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.7.9"
|
||||
@@ -212,12 +224,24 @@ version = "3.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder-lite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.1"
|
||||
@@ -280,6 +304,12 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
@@ -295,6 +325,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
@@ -406,6 +445,25 @@ version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
@@ -549,6 +607,16 @@ dependencies = [
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159"
|
||||
dependencies = [
|
||||
"color_quant",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.18"
|
||||
@@ -821,6 +889,34 @@ dependencies = [
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
"color_quant",
|
||||
"gif",
|
||||
"image-webp",
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
"png",
|
||||
"zune-core",
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image-webp"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
|
||||
dependencies = [
|
||||
"byteorder-lite",
|
||||
"quick-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
@@ -958,6 +1054,16 @@ dependencies = [
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.0"
|
||||
@@ -969,6 +1075,16 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moxcms"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"pxfm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multer"
|
||||
version = "3.1.0"
|
||||
@@ -1001,6 +1117,15 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
@@ -1015,7 +1140,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "openpxe"
|
||||
version = "0.4.6"
|
||||
version = "0.4.61"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -1037,7 +1162,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openpxe-core"
|
||||
version = "0.4.6"
|
||||
version = "0.4.61"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bcrypt",
|
||||
@@ -1056,7 +1181,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openpxe-dhcp-proxy"
|
||||
version = "0.4.6"
|
||||
version = "0.4.61"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -1070,13 +1195,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openpxe-http-api"
|
||||
version = "0.4.6"
|
||||
version = "0.4.61"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"bytes",
|
||||
"futures",
|
||||
"hyper",
|
||||
"image",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"openpxe-core",
|
||||
@@ -1100,7 +1226,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openpxe-ipxe-assets"
|
||||
version = "0.4.6"
|
||||
version = "0.4.61"
|
||||
dependencies = [
|
||||
"openpxe-core",
|
||||
"rust-embed",
|
||||
@@ -1110,12 +1236,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openpxe-iso-store"
|
||||
version = "0.4.6"
|
||||
version = "0.4.61"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bcrypt",
|
||||
"bytes",
|
||||
"hex",
|
||||
"image",
|
||||
"libc",
|
||||
"openpxe-core",
|
||||
"parking_lot",
|
||||
@@ -1133,7 +1260,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openpxe-tftp"
|
||||
version = "0.4.6"
|
||||
version = "0.4.61"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -1147,7 +1274,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openpxe-webui"
|
||||
version = "0.4.6"
|
||||
version = "0.4.61"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
@@ -1184,6 +1311,19 @@ version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.5"
|
||||
@@ -1227,6 +1367,18 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pxfm"
|
||||
version = "0.1.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
@@ -1484,6 +1636,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
@@ -2124,6 +2282,12 @@ dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
@@ -2438,3 +2602,18 @@ name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
|
||||
|
||||
[[package]]
|
||||
name = "zune-jpeg"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
|
||||
dependencies = [
|
||||
"zune-core",
|
||||
]
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.4.6"
|
||||
version = "0.4.61"
|
||||
edition = "2021"
|
||||
rust-version = "1.95"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
@@ -41,3 +41,6 @@ tower = { workspace = true }
|
||||
tempfile = "3.12"
|
||||
serde_json = { workspace = true }
|
||||
time = { workspace = true }
|
||||
# v0.4.61: integration tests need to generate real PNG bytes for the
|
||||
# `/branding/pxe-logo` compositor; hand-rolled CRCs are too fragile.
|
||||
image = { version = "0.25", default-features = false, features = ["png"] }
|
||||
|
||||
+108
-44
@@ -193,23 +193,47 @@ async fn api_sso_put(State(state): State<AppState>, Json(body): Json<SsoConfig>)
|
||||
// ─── UI ────────────────────────────────────────────────────────────────────
|
||||
|
||||
async fn index(State(state): State<AppState>) -> Response {
|
||||
let html = openpxe_webui::index_html(&state.public_base_url);
|
||||
// The asset version pin in index.html (`?v=…`) is what makes
|
||||
// browsers re-fetch JS/CSS after an upgrade. We use the OpenPXE
|
||||
// binary version — every release ships a new value, every release
|
||||
// forces a fresh URL on each asset.
|
||||
let html = openpxe_webui::index_html(&state.public_base_url, env!("CARGO_PKG_VERSION"));
|
||||
(
|
||||
[(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("text/html; charset=utf-8"),
|
||||
)],
|
||||
[
|
||||
(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("text/html; charset=utf-8"),
|
||||
),
|
||||
// index.html itself must never be cached — that's how the
|
||||
// browser learns about a new `?v=…` value for the assets.
|
||||
(
|
||||
header::CACHE_CONTROL,
|
||||
HeaderValue::from_static("no-cache, must-revalidate"),
|
||||
),
|
||||
],
|
||||
html,
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
/// Cache-Control header value used for the bundled JS/CSS/SVG assets.
|
||||
/// We pin a 1-day TTL so a long-lived deployment doesn't re-fetch the
|
||||
/// same bytes on every page-load, but require revalidation — combined
|
||||
/// with the `?v=<version>` query string in index.html, the practical
|
||||
/// upper bound on caching across an upgrade is "until the operator
|
||||
/// reloads".
|
||||
const ASSET_CACHE_CONTROL: HeaderValue =
|
||||
HeaderValue::from_static("no-cache, must-revalidate");
|
||||
|
||||
async fn ui_js() -> Response {
|
||||
(
|
||||
[(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("application/javascript"),
|
||||
)],
|
||||
[
|
||||
(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("application/javascript"),
|
||||
),
|
||||
(header::CACHE_CONTROL, ASSET_CACHE_CONTROL),
|
||||
],
|
||||
openpxe_webui::app_js(),
|
||||
)
|
||||
.into_response()
|
||||
@@ -217,7 +241,10 @@ async fn ui_js() -> Response {
|
||||
|
||||
async fn ui_css() -> Response {
|
||||
(
|
||||
[(header::CONTENT_TYPE, HeaderValue::from_static("text/css"))],
|
||||
[
|
||||
(header::CONTENT_TYPE, HeaderValue::from_static("text/css")),
|
||||
(header::CACHE_CONTROL, ASSET_CACHE_CONTROL),
|
||||
],
|
||||
openpxe_webui::app_css(),
|
||||
)
|
||||
.into_response()
|
||||
@@ -261,20 +288,29 @@ async fn ui_logo(State(state): State<AppState>) -> Response {
|
||||
}
|
||||
}
|
||||
(
|
||||
[(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("image/svg+xml"),
|
||||
)],
|
||||
[
|
||||
(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("image/svg+xml"),
|
||||
),
|
||||
(header::CACHE_CONTROL, ASSET_CACHE_CONTROL),
|
||||
],
|
||||
openpxe_webui::logo_svg(),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
/// v0.4.6: raster-only logo endpoint for the iPXE menu's
|
||||
/// `console --picture`. iPXE can't rasterize SVG, so SVG uploads 404
|
||||
/// here — the ASCII OpenPXE wordmark in `render_menu` already gives
|
||||
/// the operator a polished default. No bundled PNG fallback by design:
|
||||
/// either the operator's raster logo paints, or the text stands in.
|
||||
/// v0.4.61: PXE menu logo composed for the iPXE `console --picture`
|
||||
/// call. The operator can upload any raster image (PNG / JPEG / WebP /
|
||||
/// GIF) of any aspect ratio; this handler decodes it, draws it
|
||||
/// centered-top onto a fixed 1024×768 canvas, and returns PNG bytes.
|
||||
/// That gives the same look as iVentoy regardless of what the operator
|
||||
/// uploaded — a portrait logo, a wide wordmark, a square monogram all
|
||||
/// land in the same place on the boot screen.
|
||||
///
|
||||
/// SVG uploads still 404 here — iPXE can't rasterize SVG, and rather
|
||||
/// than haul in `resvg` we ask the operator to provide a raster when
|
||||
/// they want a custom PXE-side logo. (The WebUI keeps using the SVG.)
|
||||
async fn ui_pxe_logo(State(state): State<AppState>) -> Response {
|
||||
let Some(path) = state.branding.logo_path() else {
|
||||
return (StatusCode::NOT_FOUND, "no custom logo configured").into_response();
|
||||
@@ -285,40 +321,68 @@ async fn ui_pxe_logo(State(state): State<AppState>) -> Response {
|
||||
if mime == "image/svg+xml" {
|
||||
return (
|
||||
StatusCode::NOT_FOUND,
|
||||
"operator-uploaded logo is SVG; iPXE menu falls back to the bundled ASCII wordmark",
|
||||
"operator-uploaded logo is SVG; PXE menu requires a raster (PNG / JPEG / WebP / GIF)",
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
match tokio::fs::read(&path).await {
|
||||
Ok(bytes) => {
|
||||
let ct = HeaderValue::from_str(&mime)
|
||||
.unwrap_or_else(|_| HeaderValue::from_static("application/octet-stream"));
|
||||
(
|
||||
[
|
||||
(header::CONTENT_TYPE, ct),
|
||||
(
|
||||
header::CACHE_CONTROL,
|
||||
HeaderValue::from_static("no-cache, max-age=0"),
|
||||
),
|
||||
],
|
||||
bytes,
|
||||
let bytes = match tokio::fs::read(&path).await {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
return (
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("custom logo unreadable: {e}"),
|
||||
)
|
||||
.into_response()
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => (
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("custom logo unreadable: {e}"),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
};
|
||||
// Compose to a fixed 1024×768 PNG so the PXE menu paints the logo
|
||||
// centered-top regardless of the operator's source dimensions. The
|
||||
// `image` crate is pure-Rust + sync; offload to a blocking task
|
||||
// because Lanczos resampling on a 4K source can take tens of
|
||||
// milliseconds and we don't want to block the executor.
|
||||
let composed =
|
||||
match tokio::task::spawn_blocking(move || openpxe_iso_store::pxe_logo::compose_pxe_logo(&bytes))
|
||||
.await
|
||||
{
|
||||
Ok(Ok(png)) => png,
|
||||
Ok(Err(e)) => {
|
||||
tracing::warn!(
|
||||
target: "openpxe::http::branding",
|
||||
error = %e, "failed to compose PXE logo PNG"
|
||||
);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("failed to compose PXE logo: {e}"),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("pxe-logo task failed: {e}"),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
(
|
||||
[
|
||||
(header::CONTENT_TYPE, HeaderValue::from_static("image/png")),
|
||||
(header::CACHE_CONTROL, ASSET_CACHE_CONTROL),
|
||||
],
|
||||
composed,
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn ui_loader() -> Response {
|
||||
(
|
||||
[(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("image/svg+xml"),
|
||||
)],
|
||||
[
|
||||
(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("image/svg+xml"),
|
||||
),
|
||||
(header::CACHE_CONTROL, ASSET_CACHE_CONTROL),
|
||||
],
|
||||
openpxe_webui::loader_svg(),
|
||||
)
|
||||
.into_response()
|
||||
|
||||
@@ -77,17 +77,15 @@ pub fn render_menu(isos: &[IsoMeta], settings: &Settings, base_url: &str) -> Str
|
||||
);
|
||||
let _ = writeln!(s, ":menu");
|
||||
let _ = writeln!(s, "menu OpenPXE - network boot menu");
|
||||
// Centered ASCII wordmark. iPXE menus are ~76 columns wide on the
|
||||
// default VGA text console; the lines below are padded to sit
|
||||
// approximately centered. `item --gap -- <text>` emits text without
|
||||
// a selectable hotkey.
|
||||
let _ = writeln!(s, "item --gap");
|
||||
let _ = writeln!(s, "item --gap -- ___ ___ __ __ ___");
|
||||
let _ = writeln!(s, "item --gap -- / _ \\ _ __ ___ _ _ | _ \\ \\/ / | __|");
|
||||
let _ = writeln!(s, "item --gap -- | (_) | '_ \\/ -_) ' \\ | _/ \\ / | _|");
|
||||
let _ = writeln!(s, "item --gap -- \\___/| .__/\\___|_||_| |_| /_/\\_\\ |___|");
|
||||
let _ = writeln!(s, "item --gap -- |_|");
|
||||
let _ = writeln!(s, "item --gap");
|
||||
// v0.4.61: we used to draw an ASCII OpenPXE wordmark here. Now
|
||||
// that the bundled iPXE is built with `IMAGE_PNG`, the
|
||||
// `console --picture` line at the top of this script paints the
|
||||
// operator's actual logo (composed server-side into a 1024×768
|
||||
// canvas with the logo centered at the top) — the ASCII banner
|
||||
// became visual noise *on top* of the real image. Old iPXE
|
||||
// builds without PNG fall through the `|| console` clause and
|
||||
// simply show the menu without a logo, which is the correct
|
||||
// graceful-degradation outcome.
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"item --gap -- ------------------------- Default -------------------------"
|
||||
@@ -628,10 +626,12 @@ mod password_tests {
|
||||
|
||||
#[test]
|
||||
fn top_menu_has_polished_branding_and_arch_footer() {
|
||||
// v0.4.6 polish: a `console --picture` line for operator
|
||||
// logos, an ASCII OpenPXE wordmark visible across iPXE
|
||||
// builds (graphics or not), and a single-line footer carrying
|
||||
// the current OpenPXE version + the resolved arch label.
|
||||
// v0.4.6 polish + v0.4.61 image upgrade: the menu emits a
|
||||
// `console --picture` line that the bundled iPXE (built with
|
||||
// `IMAGE_PNG`) honours, plus an arch-resolved footer carrying
|
||||
// the current OpenPXE version. The ASCII wordmark that used
|
||||
// to live here was dropped in v0.4.61 — it duplicated the now-
|
||||
// working image.
|
||||
let settings = Settings::default();
|
||||
let s = render_menu(&[], &settings, "http://10.0.0.5");
|
||||
assert!(
|
||||
@@ -641,11 +641,11 @@ mod password_tests {
|
||||
// Picture-or-text-console must be a single statement so older
|
||||
// iPXE parsers don't choke on the chain.
|
||||
assert!(s.contains("|| console"), "missing graceful fallback:\n{s}");
|
||||
// ASCII wordmark — at least one of the banner lines must
|
||||
// contain the trailing pipe segment, plus the leading "_"s.
|
||||
// No ASCII wordmark — once the real PNG paints, the ASCII
|
||||
// banner would duplicate the operator's logo visually.
|
||||
assert!(
|
||||
s.contains("___ ___ __ __ ___"),
|
||||
"ascii banner missing first row:\n{s}"
|
||||
!s.contains("___ ___ __ __ ___"),
|
||||
"ASCII banner shouldn't be emitted in v0.4.61+:\n{s}"
|
||||
);
|
||||
// Footer with version + arch interpolation. The version comes
|
||||
// from CARGO_PKG_VERSION at compile time.
|
||||
|
||||
@@ -1755,11 +1755,26 @@ async fn pxe_logo_404_when_no_custom_logo_configured() {
|
||||
assert!(text.contains("no custom logo"), "got: {text}");
|
||||
}
|
||||
|
||||
/// Build a tiny valid PNG via the `image` crate. The v0.4.61 PXE-logo
|
||||
/// compositor decodes whatever the operator uploaded — hand-rolled
|
||||
/// PNGs with handwritten CRCs are too easy to break; let the encoder
|
||||
/// produce something it can later decode.
|
||||
fn tiny_png() -> Vec<u8> {
|
||||
use image::{DynamicImage, ImageBuffer, ImageFormat, Rgb};
|
||||
use std::io::Cursor;
|
||||
let buf: ImageBuffer<Rgb<u8>, Vec<u8>> = ImageBuffer::from_pixel(8, 8, Rgb([0, 180, 220]));
|
||||
let mut out = Vec::with_capacity(256);
|
||||
DynamicImage::ImageRgb8(buf)
|
||||
.write_to(&mut Cursor::new(&mut out), ImageFormat::Png)
|
||||
.unwrap();
|
||||
out
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pxe_logo_404_when_uploaded_logo_is_svg() {
|
||||
// iPXE can't rasterize SVG, so an SVG upload deliberately doesn't
|
||||
// light up the PXE menu's `console --picture` overlay — the ASCII
|
||||
// wordmark in render_menu stands in instead.
|
||||
// light up the PXE menu's `console --picture` overlay — the menu
|
||||
// simply paints without a logo.
|
||||
let (state, _dir) = build_state().await;
|
||||
state
|
||||
.branding
|
||||
@@ -1777,11 +1792,15 @@ async fn pxe_logo_404_when_uploaded_logo_is_svg() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pxe_logo_serves_raster_with_correct_mime() {
|
||||
async fn pxe_logo_composes_to_1024x768_png() {
|
||||
// v0.4.61: the endpoint no longer serves the raw upload — it
|
||||
// composes the operator's logo into a fixed 1024×768 canvas so
|
||||
// the iPXE menu always paints at consistent dimensions.
|
||||
let (state, _dir) = build_state().await;
|
||||
let png = tiny_png();
|
||||
state
|
||||
.branding
|
||||
.set_logo("image/png", "png", b"\x89PNG\r\n\x1a\nfake-png-bytes")
|
||||
.set_logo("image/png", "png", &png)
|
||||
.unwrap();
|
||||
let app = build_router(state);
|
||||
let res = app
|
||||
@@ -1805,7 +1824,14 @@ async fn pxe_logo_serves_raster_with_correct_mime() {
|
||||
let body = axum::body::to_bytes(res.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap();
|
||||
// PNG signature.
|
||||
assert!(body.starts_with(b"\x89PNG"), "PNG header missing");
|
||||
// IHDR chunk lives at bytes 8..29; width is bytes 16..20, height
|
||||
// 20..24 in big-endian u32. The composed canvas should be 1024×768.
|
||||
let width = u32::from_be_bytes([body[16], body[17], body[18], body[19]]);
|
||||
let height = u32::from_be_bytes([body[20], body[21], body[22], body[23]]);
|
||||
assert_eq!(width, 1024, "compose should pin width to 1024");
|
||||
assert_eq!(height, 768, "compose should pin height to 768");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1814,9 +1840,10 @@ async fn pxe_logo_endpoint_is_public_after_admin_setup() {
|
||||
// must stay reachable once the admin has been bootstrapped. The
|
||||
// auth allowlist gates `/api/*` only.
|
||||
let (state, _dir) = build_state().await;
|
||||
let png = tiny_png();
|
||||
state
|
||||
.branding
|
||||
.set_logo("image/png", "png", b"\x89PNG\r\n\x1a\nfake")
|
||||
.set_logo("image/png", "png", &png)
|
||||
.unwrap();
|
||||
let app = build_router(state);
|
||||
// Configure an admin so the middleware kicks in.
|
||||
|
||||
@@ -27,6 +27,13 @@ parking_lot.workspace = true
|
||||
bytes.workspace = true
|
||||
tempfile = "3.12"
|
||||
libc = "0.2"
|
||||
# v0.4.61: server-side compose of the operator's uploaded raster into a
|
||||
# fixed 1024x768 canvas so the PXE menu always gets a consistently-sized
|
||||
# PNG regardless of what the operator uploaded. We use the bare-bones
|
||||
# `image` crate (no default features) and explicitly enable only the
|
||||
# decoders we accept on upload (PNG/JPEG/WebP/GIF) plus the PNG
|
||||
# encoder. Keeps the build slim — no JPEG2000, TIFF, BMP, etc.
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.12"
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
pub mod entry;
|
||||
pub mod introspect;
|
||||
pub mod nfs;
|
||||
pub mod pxe_logo;
|
||||
pub mod smb;
|
||||
pub mod store;
|
||||
pub mod windows;
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
//! Operator-logo compositor for the iPXE menu.
|
||||
//!
|
||||
//! The brief: match iVentoy's polished centered-logo PXE chrome with
|
||||
//! whatever raster the operator drops onto Settings → Branding. A wide
|
||||
//! wordmark, a portrait stack, a square monogram — all three should
|
||||
//! land in roughly the same place on the boot screen.
|
||||
//!
|
||||
//! Approach: decode the operator's upload, fit it into a fixed
|
||||
//! 1024×768 canvas with the logo horizontally centered and pinned a
|
||||
//! short margin from the top, re-encode as PNG, return the bytes. iPXE
|
||||
//! built with `IMAGE_PNG` paints the result via `console --picture`.
|
||||
//!
|
||||
//! The 1024×768 size matches the default VESA framebuffer iPXE picks
|
||||
//! on most BIOS/UEFI consoles. Operators uploading 4K logos get
|
||||
//! correctly downscaled; tiny icons get drawn at their native size,
|
||||
//! centered, with transparent margins.
|
||||
//!
|
||||
//! We deliberately don't ship `resvg` for SVG support — keeping the
|
||||
//! dependency surface narrow matters more than supporting SVG-only
|
||||
//! brand assets. The WebUI's logo stays SVG-native (the browser
|
||||
//! rasterizes it); the PXE menu wants a raster regardless.
|
||||
|
||||
use image::imageops::FilterType;
|
||||
use image::{DynamicImage, ImageError, ImageFormat, Rgba, RgbaImage};
|
||||
use std::io::Cursor;
|
||||
|
||||
/// Canvas dimensions used for the composed PXE logo. Picked to match
|
||||
/// the framebuffer dimensions iPXE picks on most BIOS/UEFI consoles —
|
||||
/// gives a 1:1 paint with no scaling at the firmware layer.
|
||||
pub const CANVAS_W: u32 = 1024;
|
||||
pub const CANVAS_H: u32 = 768;
|
||||
|
||||
/// Maximum dimensions for the operator's logo inside the canvas. Any
|
||||
/// upload larger than this in either axis is downscaled (preserving
|
||||
/// aspect ratio) to fit. Smaller uploads paint at native size.
|
||||
const LOGO_MAX_W: u32 = 600;
|
||||
const LOGO_MAX_H: u32 = 200;
|
||||
|
||||
/// Top margin in pixels from the canvas's top edge to the logo's top
|
||||
/// edge. Matches the visual rhythm of iVentoy's screen (logo at top,
|
||||
/// menu below).
|
||||
const LOGO_TOP_MARGIN: u32 = 64;
|
||||
|
||||
/// Compose `src_bytes` (any PNG/JPEG/WebP/GIF) into a centered-top
|
||||
/// 1024×768 PNG and return the encoded bytes.
|
||||
///
|
||||
/// Errors when the source can't be decoded or the encoded buffer can't
|
||||
/// be written (only really fires on out-of-memory; the encoder itself
|
||||
/// is infallible for well-formed inputs).
|
||||
pub fn compose_pxe_logo(src_bytes: &[u8]) -> Result<Vec<u8>, ImageError> {
|
||||
let logo = image::load_from_memory(src_bytes)?;
|
||||
// Resize-fit if the upload exceeds our bounding box. `Lanczos3`
|
||||
// keeps the antialiasing crisp on the framebuffer console; it's a
|
||||
// touch slower than `Triangle` but the operator hits this endpoint
|
||||
// once per boot at most.
|
||||
let logo = downscale_to_fit(logo, LOGO_MAX_W, LOGO_MAX_H);
|
||||
let logo_rgba = logo.to_rgba8();
|
||||
|
||||
// Transparent canvas. iPXE 1.21+ honours alpha-channel transparency
|
||||
// on framebuffer consoles; older builds simply draw the alpha as
|
||||
// black, which still gives a sensible look.
|
||||
let mut canvas: RgbaImage = RgbaImage::from_pixel(CANVAS_W, CANVAS_H, Rgba([0, 0, 0, 0]));
|
||||
let logo_w = logo_rgba.width();
|
||||
let logo_h = logo_rgba.height();
|
||||
// Horizontal center, top-margin from the top. Saturating math
|
||||
// means a logo wider than CANVAS_W (shouldn't happen after the
|
||||
// downscale above, but defensive) just sits flush-left.
|
||||
let off_x = CANVAS_W.saturating_sub(logo_w) / 2;
|
||||
let off_y = LOGO_TOP_MARGIN.min(CANVAS_H.saturating_sub(logo_h));
|
||||
image::imageops::overlay(&mut canvas, &logo_rgba, off_x.into(), off_y.into());
|
||||
|
||||
let mut out = Vec::with_capacity(64 * 1024);
|
||||
DynamicImage::ImageRgba8(canvas).write_to(&mut Cursor::new(&mut out), ImageFormat::Png)?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn downscale_to_fit(img: DynamicImage, max_w: u32, max_h: u32) -> DynamicImage {
|
||||
let (w, h) = (img.width(), img.height());
|
||||
if w <= max_w && h <= max_h {
|
||||
return img;
|
||||
}
|
||||
// Preserve aspect ratio. `resize` clamps to the smaller of the
|
||||
// two scale factors so we never overshoot the bounding box.
|
||||
img.resize(max_w, max_h, FilterType::Lanczos3)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use image::{ImageBuffer, Rgb};
|
||||
|
||||
fn solid_png(w: u32, h: u32, rgb: [u8; 3]) -> Vec<u8> {
|
||||
let img: ImageBuffer<Rgb<u8>, Vec<u8>> = ImageBuffer::from_pixel(w, h, Rgb(rgb));
|
||||
let mut out = Vec::with_capacity(4096);
|
||||
DynamicImage::ImageRgb8(img)
|
||||
.write_to(&mut Cursor::new(&mut out), ImageFormat::Png)
|
||||
.unwrap();
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compose_emits_canvas_sized_png() {
|
||||
let src = solid_png(120, 60, [200, 50, 50]);
|
||||
let out = compose_pxe_logo(&src).unwrap();
|
||||
// Round-trip the output and confirm dimensions.
|
||||
let img = image::load_from_memory(&out).unwrap();
|
||||
assert_eq!(img.width(), CANVAS_W);
|
||||
assert_eq!(img.height(), CANVAS_H);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_logo_centered_at_top_margin() {
|
||||
let src = solid_png(100, 40, [10, 200, 10]);
|
||||
let out = compose_pxe_logo(&src).unwrap();
|
||||
let canvas = image::load_from_memory(&out).unwrap().to_rgba8();
|
||||
// Pixel just inside the logo box should match the source color
|
||||
// (alpha=255). Pixel near a far corner of the canvas should be
|
||||
// the transparent background.
|
||||
let cx = (CANVAS_W - 100) / 2;
|
||||
let cy = LOGO_TOP_MARGIN;
|
||||
let inside = canvas.get_pixel(cx + 10, cy + 10);
|
||||
assert_eq!(inside.0[3], 255, "logo pixel should be opaque");
|
||||
assert!(inside.0[0] < 100 && inside.0[1] > 100 && inside.0[2] < 100, "color mismatch: {inside:?}");
|
||||
let corner = canvas.get_pixel(CANVAS_W - 1, CANVAS_H - 1);
|
||||
assert_eq!(corner.0[3], 0, "canvas corner should be transparent");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversize_logo_is_downscaled_to_bounding_box() {
|
||||
// 4000×800 image — bigger than LOGO_MAX_W and LOGO_MAX_H in
|
||||
// both axes. After downscale the output must fit; we re-decode
|
||||
// the canvas, count non-transparent pixels, and confirm none
|
||||
// sit outside the expected band.
|
||||
let src = solid_png(4000, 800, [50, 50, 200]);
|
||||
let out = compose_pxe_logo(&src).unwrap();
|
||||
let canvas = image::load_from_memory(&out).unwrap().to_rgba8();
|
||||
// Span row at the top margin should have non-transparent
|
||||
// pixels somewhere; rows past the LOGO_TOP_MARGIN + LOGO_MAX_H
|
||||
// should be entirely transparent.
|
||||
let bottom_band_y = LOGO_TOP_MARGIN + LOGO_MAX_H + 10;
|
||||
for x in 0..CANVAS_W {
|
||||
let p = canvas.get_pixel(x, bottom_band_y);
|
||||
assert_eq!(p.0[3], 0, "row {bottom_band_y} should be transparent at x={x}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_bytes_returns_error_not_panic() {
|
||||
let r = compose_pxe_logo(b"\xde\xad\xbe\xef not an image");
|
||||
assert!(r.is_err());
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,15 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="color-scheme" content="dark light" />
|
||||
<title>OpenPXE</title>
|
||||
<link rel="stylesheet" href="/assets/app.css" />
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg" />
|
||||
<!-- v0.4.61: the `?v=…` query string is replaced by the server at
|
||||
request time with the running OpenPXE version. That guarantees a
|
||||
fresh URL on every upgrade so browsers (and intermediary proxies)
|
||||
can't keep serving stale JS / CSS / branding from before the
|
||||
deploy. Combined with `Cache-Control: no-cache, must-revalidate`
|
||||
on the asset handlers, the practical caching window is one
|
||||
version. -->
|
||||
<link rel="stylesheet" href="/assets/app.css?v={{ASSET_VERSION}}" />
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg?v={{ASSET_VERSION}}" />
|
||||
<!-- Theme is read from localStorage *before* paint to avoid the
|
||||
dark→light flash on every navigation. Falls back to the OS
|
||||
preference and finally to dark. -->
|
||||
@@ -26,7 +33,7 @@
|
||||
<div class="shell">
|
||||
<aside class="sidebar">
|
||||
<div class="brand">
|
||||
<img src="/assets/logo.svg" alt="OpenPXE" />
|
||||
<img src="/assets/logo.svg?v={{ASSET_VERSION}}" alt="OpenPXE" />
|
||||
<strong>OpenPXE</strong>
|
||||
</div>
|
||||
<nav>
|
||||
@@ -59,7 +66,7 @@
|
||||
<!-- The brand badge at the top can be overridden by operator-uploaded
|
||||
logos; keep "OpenPXE v…" pinned in the footer so the backend
|
||||
identity is always visible regardless of branding. -->
|
||||
<div class="footer-version">OpenPXE v<span data-bind="version">0.4.6</span></div>
|
||||
<div class="footer-version">OpenPXE v<span data-bind="version">0.4.61</span></div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -115,6 +122,6 @@
|
||||
<main class="main" id="view-root"></main>
|
||||
</div>
|
||||
|
||||
<script src="/assets/app.js"></script>
|
||||
<script src="/assets/app.js?v={{ASSET_VERSION}}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+14
-4
@@ -7,11 +7,21 @@
|
||||
//! nav, top bar with secondary tabs, card-dense content panels.
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
/// Render the top-level page. `base_url` is interpolated into the footer
|
||||
/// so operators can see at a glance what URL clients are PXE-booting from.
|
||||
/// Render the top-level page.
|
||||
///
|
||||
/// * `base_url` is interpolated into the footer so operators can see at
|
||||
/// a glance what URL clients are PXE-booting from.
|
||||
/// * `asset_version` is appended as `?v=…` to every asset URL so each
|
||||
/// release ships with brand-new asset URLs — browsers (and any
|
||||
/// intermediary proxy) can't keep serving last release's `app.js`
|
||||
/// when we know the new one is incompatible. Combined with
|
||||
/// `Cache-Control: no-cache, must-revalidate` on the asset handlers,
|
||||
/// the worst-case caching window is one version.
|
||||
#[must_use]
|
||||
pub fn index_html(base_url: &str) -> String {
|
||||
INDEX_HTML.replace("{{BASE_URL}}", base_url)
|
||||
pub fn index_html(base_url: &str, asset_version: &str) -> String {
|
||||
INDEX_HTML
|
||||
.replace("{{BASE_URL}}", base_url)
|
||||
.replace("{{ASSET_VERSION}}", asset_version)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
|
||||
@@ -16,13 +16,79 @@
|
||||
|
||||
ARG RUST_VERSION=1.95
|
||||
|
||||
########## fetch iPXE binaries ##########
|
||||
########## fetch wimboot (and a sanity-check fetch of upstream iPXE) ##########
|
||||
# v0.4.61: we no longer ship the boot.ipxe.org iPXE binaries directly;
|
||||
# instead we build iPXE from source with IMAGE_PNG enabled (see the
|
||||
# ipxe-build stage below). The fetch stage still pulls wimboot (a
|
||||
# pre-signed binary from ipxe/wimboot's GitHub release) since that's
|
||||
# unrelated to the PNG concern.
|
||||
FROM debian:12-slim AS fetch
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /src
|
||||
COPY scripts/fetch-ipxe.sh scripts/fetch-ipxe.sh
|
||||
RUN mkdir -p assets/ipxe && bash scripts/fetch-ipxe.sh
|
||||
RUN mkdir -p assets/ipxe && \
|
||||
curl --fail --silent --show-error --location \
|
||||
-o assets/ipxe/wimboot \
|
||||
https://github.com/ipxe/wimboot/releases/latest/download/wimboot \
|
||||
|| echo "wimboot fetch failed; Windows toggle will stay disabled"
|
||||
|
||||
########## build iPXE from source with IMAGE_PNG enabled ##########
|
||||
# This stage replaces the old "grab pre-built binaries from
|
||||
# boot.ipxe.org" path. The shipped binaries there are built with the
|
||||
# default config which omits `IMAGE_PNG`, so the `console --picture`
|
||||
# call in render_menu silently no-ops — operator logos never paint.
|
||||
# Building from source lets us flip the one flag we need.
|
||||
#
|
||||
# Cross-compilation: x86_64 + i386 use the native toolchain that ships
|
||||
# in the rust:bookworm base; arm64 uses gcc-aarch64-linux-gnu. The four
|
||||
# output binaries match the names openpxe-ipxe-assets expects in
|
||||
# assets/ipxe/.
|
||||
FROM rust:${RUST_VERSION}-bookworm AS ipxe-build
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
git build-essential liblzma-dev mtools genisoimage syslinux \
|
||||
gcc-aarch64-linux-gnu \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /build
|
||||
# Pin to a recent iPXE master tip via shallow clone. iPXE doesn't tag
|
||||
# releases; pinning the SHA in source would be a periodic chore. The
|
||||
# tradeoff is that "rebuild the container" silently picks up upstream
|
||||
# patches — for a boot loader this is the right side of the
|
||||
# pin-vs-fresh tradeoff (we want CVE fixes ASAP and the PXE chain is
|
||||
# the trusted base).
|
||||
RUN git clone --depth=1 https://github.com/ipxe/ipxe.git ipxe
|
||||
WORKDIR /build/ipxe/src
|
||||
# Feature flags landed via the `config/local/` override files iPXE's
|
||||
# config system reads after `config/general.h`. We enable just the
|
||||
# image format + framebuffer console plumbing — everything else stays
|
||||
# at the upstream default. `keep-debug` is off; `parserrors` is off; we
|
||||
# pin a small set of useful tweaks.
|
||||
RUN mkdir -p config/local \
|
||||
&& printf '%s\n' \
|
||||
'#define IMAGE_PNG' \
|
||||
'#define CONSOLE_FRAMEBUFFER' \
|
||||
'#define CONSOLE_VESAFB' \
|
||||
'#define DOWNLOAD_PROTO_HTTPS' \
|
||||
'#define NSLOOKUP_CMD' \
|
||||
'#define NTP_CMD' \
|
||||
> config/local/general.h
|
||||
# Each arch builds to its own `bin-*` directory. We copy the four
|
||||
# output binaries into /out/ with the names openpxe-ipxe-assets
|
||||
# expects. Stripping the binaries saves ~30% — they go into the rust
|
||||
# binary via include_bytes! so the savings ripple through the final
|
||||
# image.
|
||||
RUN mkdir -p /out && \
|
||||
make -j"$(nproc)" bin/undionly.kpxe && \
|
||||
cp bin/undionly.kpxe /out/undionly.kpxe && \
|
||||
make -j"$(nproc)" bin-x86_64-efi/snponly.efi && \
|
||||
cp bin-x86_64-efi/snponly.efi /out/snponly.efi && \
|
||||
make -j"$(nproc)" bin-x86_64-efi/ipxe.efi && \
|
||||
cp bin-x86_64-efi/ipxe.efi /out/ipxe.efi && \
|
||||
make -j"$(nproc)" bin-i386-efi/snponly.efi && \
|
||||
cp bin-i386-efi/snponly.efi /out/snponly-i386.efi && \
|
||||
make -j"$(nproc)" CROSS_COMPILE=aarch64-linux-gnu- bin-arm64-efi/snponly.efi && \
|
||||
cp bin-arm64-efi/snponly.efi /out/snponly-arm64.efi && \
|
||||
ls -lh /out/
|
||||
|
||||
########## build openpxe ##########
|
||||
FROM rust:${RUST_VERSION}-bookworm AS build
|
||||
@@ -56,7 +122,11 @@ RUN apt-get update \
|
||||
# `cargo build`, which is slow and can exhaust small Colima/CI disks.
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY crates/ crates/
|
||||
COPY --from=fetch /src/assets/ipxe /src/assets/ipxe
|
||||
# v0.4.61: iPXE binaries come from our own source-built stage with
|
||||
# IMAGE_PNG enabled. wimboot still comes from the fetch stage (it's
|
||||
# from ipxe/wimboot's GitHub release, separately signed).
|
||||
COPY --from=ipxe-build /out/ /src/assets/ipxe/
|
||||
COPY --from=fetch /src/assets/ipxe/wimboot /src/assets/ipxe/wimboot
|
||||
|
||||
# Cache cargo registry + target across builds. The mtime touch is
|
||||
# belt-and-suspenders: cargo occasionally misses mtime-only changes on
|
||||
|
||||
Reference in New Issue
Block a user