Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
[alias]
xtask = "run --package xtask --"

[build]
# The purpose of this flag is to block crates using version_detect from "detecting"
# features that are no longer supported by the toolchain, because despite its name,
# version_detect is basically "if nightly { return true; }". This setting gets
# overridden within xtask for Hubris programs, so this only affects host tools like
# xtask.
rustflags = ["-Zallow-features=proc_macro_diagnostic,used_with_arg"]

[unstable]
bindeps = true
19 changes: 10 additions & 9 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ members = [
]
default-members = []
resolver = "2"
rust-version = "1.89.0"

[profile.release]
codegen-units = 1 # better optimizations
Expand Down Expand Up @@ -111,6 +112,7 @@ rangemap = { version = "1.3", default-features = false }
regex = { version = "1", default-features = false, features = ["std", "perf", "unicode-perl"] }
ron = { version = "0.8", default-features = false }
rustc-demangle = { version = "0.1.24", default-features = false }
rustc_version = { version = "0.4.1" }
scroll = { version = "0.10", default-features = false }
serde = { version = "1.0.114", default-features = false, features = ["derive"] }
serde-big-array = { version = "0.4", default-features = false }
Expand Down
28 changes: 8 additions & 20 deletions FAQ.mkdn
Original file line number Diff line number Diff line change
Expand Up @@ -129,28 +129,16 @@ try to keep things working on Mac and Illumos.

### What Rust toolchain versions do you support?

Because of our reliance on nightly toolchain features, we support exactly one
version of the toolchain -- the one listed in our `rust-toolchain.toml` file at
the top of the repo.
Hubris can be built using the stable toolchain (see [Cargo.toml](Cargo.toml) for
the MSRV). This should suffice for development uses and production builds.

### Why does Hubris require the nightly Rust toolchain?

Mostly because we need some assembly language, and we'd like to write that in
Rust functions instead of separate files, which means we need the unstable `asm`
feature. To do context switching, specifically, we need to be able to write a
function that contains _only_ assembly instructions without additional
preamble/epilogue code. For this, we need the unstable `naked_fn` feature.

At the time of this writing, portions of `asm` are on track to stabilizing, but
unfortunately not the portions we use.

Unfortunately, Hubris's use of the nightly toolchain more or less requires any
applications of Hubris to also use the nightly toolchain.

Just because we're on nightly doesn't mean we can use any unstable feature --
we're trying hard _not_ to use any additional unstable features, in the hopes of
eventually being able to use stable.
There is one feature that requires nightly: build-time task [stack overflow
checking]. Using this check requires [emit-stack-sizes], for which there is
no stable workaround or equivalent. In order to enable this check, pass the
flag `+nightly-2025-07-20` to cargo (i.e. `cargo +nightly... xtask ...`).

[stack overflow checking]: https://github.com/oxidecomputer/hubris/pull/1890
[emit-stack-sizes]: https://doc.rust-lang.org/beta/unstable-book/compiler-flags/emit-stack-sizes.html

## Hardware support

Expand Down
1 change: 1 addition & 0 deletions build/xtask/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ serde = { workspace = true }
serde_json = { workspace = true }
sha3 = { workspace = true }
rustc-demangle = { workspace = true }
rustc_version = { workspace = true }
tlvc = { workspace = true }
tlvc-text = { workspace = true }
toml = { workspace = true }
Expand Down
14 changes: 0 additions & 14 deletions build/xtask/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -797,20 +797,6 @@ impl BuildConfig<'_> {
None => PathBuf::from("cargo"),
});

let mut nightly_features = vec![];
// nightly features that we use:
nightly_features.extend(["emit_stack_sizes", "used_with_arg"]);
// nightly features that our dependencies use:
nightly_features.extend([
"backtrace",
"error_generic_member_access",
"proc_macro_span",
"proc_macro_span_shrink",
"provide_any",
]);

cmd.arg(format!("-Zallow-features={}", nightly_features.join(",")));

cmd.arg(subcommand);
cmd.arg("-p").arg(&self.crate_name);
for a in &self.args {
Expand Down
174 changes: 146 additions & 28 deletions build/xtask/src/dist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ use std::process::{Command, Stdio};

use anyhow::{anyhow, bail, Context, Result};
use atty::Stream;
use cargo_metadata::Message;
use indexmap::IndexMap;
use lpc55_rom_data::FLASH_PAGE_SIZE as LPC55_FLASH_PAGE_SIZE;
use multimap::MultiMap;
use path_slash::{PathBufExt, PathExt};
use rustc_version::{version_meta, Channel};
use sha3::{Digest, Sha3_256};
use zerocopy::IntoBytes;

Expand Down Expand Up @@ -373,6 +375,13 @@ pub fn package(
// return value, because we're going to link them regardless of whether the
// build changed.
for name in cfg.toml.tasks.keys() {
// Magic for lpc55-swd to be able to include endoscope, until artifact
// dependencies are available on stable. See build_endoscope's comment for
// more.
if cfg.toml.tasks[name.as_str()].name == "drv-lpc55-swd" {
build_endoscope()?;
}

if tasks_to_build.contains(name.as_str()) {
build_task(&cfg, name)?;
}
Expand Down Expand Up @@ -493,22 +502,24 @@ pub fn package(
})
.collect::<Result<_, _>>()?;

// Check stack sizes and resolve task slots in our linked files
let mut possible_stack_overflow = vec![];
for task_name in cfg.toml.tasks.keys() {
if tasks_to_build.contains(task_name.as_str()) {
if task_can_overflow(&cfg.toml, task_name, verbose)? {
possible_stack_overflow.push(task_name);
}
if version_meta().unwrap().channel == Channel::Nightly {
// Check stack sizes and resolve task slots in our linked files
let mut possible_stack_overflow = vec![];
for task_name in cfg.toml.tasks.keys() {
if tasks_to_build.contains(task_name.as_str()) {
if task_can_overflow(&cfg.toml, task_name, verbose)? {
possible_stack_overflow.push(task_name);
}

resolve_task_slots(&cfg, task_name, image_name)?;
resolve_task_slots(&cfg, task_name, image_name)?;
}
}
if !possible_stack_overflow.is_empty() {
bail!(
"tasks may overflow: {possible_stack_overflow:?}; \
see logs above"
);
Comment on lines +505 to +521
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooooh this is really a deal breaker right now I think :/

We really need the stack size checking feature as stackoverflows are one of the biggest hubris pain points

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's okay - the work is done, and going from "here is a list of nightly features we use" to "here is the one thing we use nightly for" is an improvement. and my goal was to knock out an easy task to familiarise myself with hubris, so: mission accomplished.

lmk if I should carve this change out slash refactor this PR as getting 90% there, or if there's more internal discussion to be had.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I confess I haven't looked too closely at the mechanism or how it is implemented in practice, but I wonder if we need it in production? Or could we make it part of a CI pipeline with RUSTC_BOOTSTRAP=1?

Copy link
Author

@thenewwazoo thenewwazoo Aug 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two aspects to the stack size check vis-a-vis compilation:

The first aspect is the tricker one. Right now, my current design expects cargo to be invoked with +nightly-... at the topmost level (i.e. cargo +nightly-... xtask ...) in order to implicitly select the nightly compiler. The rustc façade will take a toolchain spec, so the toolchain selection could potentially be pushed into the dist task by conditionally adding a +nightly-... argument to rustc. The BuildConfig used to locate rustc appears to support using a façade, though its default construction specifies the sysroot, which I think implies deeper plumbing would be necessary. The net result of this would be a stable compiler building a dist build task which invokes a nightly compiler to compile firmware tasks.

The second aspect is easier. If we shouldn't expect to find stack size info, just eat the error and move on.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about this a bit more, the stack checking is not something that ends up in the production image, if that makes sense. You could use nightly to build a binary, emitting stack size info as a side-effect, and then throw that binary away in favor of a stable-toolchain-built binary, assuming all else is equal (... a big assumption, maybe).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that was kind of what I was going for with my earlier comment about only doing the stack checks with in CI, and not production. I apologize for being terribly vague there, however.

But a two-step process, where one builds a binary with the stack size data via setting RUSTC_BOOTSTRAP=1 and setting the magic -Z flag in RUSTFLAGS, and then building another binary without setting RUSTC_BOOTSTRAP, was what I meant.

I presume we can use the same compiler binary for both invocations, and that we don't need an actual nightly compiler binary for either, as long as we set RUSTC_BOOTSTRAP=1 for the case where we need the stack data.

}
}
if !possible_stack_overflow.is_empty() {
bail!(
"tasks may overflow: {possible_stack_overflow:?}; \
see logs above"
);
}

// Add an empty output section for the caboose
Expand Down Expand Up @@ -1091,6 +1102,95 @@ struct LoadSegment {
data: Vec<u8>,
}

/// Build the endoscope binary here because it can't be built by stable cargo yet
///
/// This function invokes cargo directly in order to build the endoscope crate's binary target.
/// Endoscope is a dependency of the lpc55-swd driver, but until artifact dependencies are
/// stabilized, cargo cannot be used to build it.
///
/// For more see https://github.com/rust-lang/cargo/issues/9096
///
/// Once it's built and the environment variable is set, lpc55-swd's `build.rs` file is able to
/// find, and include, the binary's bytes in its output.
fn build_endoscope() -> Result<()> {
let mut cmd = Command::new(std::env::var("CARGO")?);
cmd.args([
"build",
"--release",
"-p",
"endoscope",
"--target",
"thumbv7em-none-eabihf",
"--features",
"soc_stm32h753",
"--message-format",
"json",
]);
cmd.env("CARGO_TARGET_DIR", "target"); // share target dir (optional)
let output = cmd.stdout(Stdio::piped()).spawn()?.wait_with_output()?;

// inspect cargo output to determine whether the build failed
let did_cargo_succeed = Message::parse_stream(output.stdout.as_slice())
.filter_map(|msg| {
if let Ok(Message::BuildFinished(bf)) = msg {
Some(bf.success)
} else {
None
}
})
.all(|s| s);

// if cargo did not succeed or exited with a non-success exit code, grab the rendered output
// from each compiler message and try to build something approximating normal compiler output.
if !did_cargo_succeed || !output.status.success() {
return Err(anyhow!(
"building endoscope did not succeed: \n{}",
Message::parse_stream(output.stdout.as_slice())
.filter_map(|msg| {
if let Ok(Message::CompilerMessage(msg)) = msg {
Some(
String::from_utf8(
msg.message
.rendered
.unwrap_or("no output rendered".to_string())
.into(),
)
.unwrap_or("output is not valid utf-8".to_string()),
)
} else {
None
}
})
.collect::<Vec<String>>()
.join("\n")
));
}

// Parse path to the produced ELF
let mut path = None;
for msg in Message::parse_stream(output.stdout.as_slice()).flatten() {
if let Message::CompilerArtifact(art) = msg {
if art.target.kind.iter().any(|k| k == "bin")
&& art.target.name == "endoscope"
{
path = art.executable.clone(); // Some(PathBuf)
}
}
}
match path {
Some(path) => {
// setting this env var lets the lpc55-swd `build.rs` file know where to find the built
// binary. this would otherwise be done by cargo as part of artifact dependency
// support.
std::env::set_var("CARGO_BIN_FILE_ENDOSCOPE", &path);
Ok(())
}
None => Err(anyhow!(
"endoscope was built but the binary could not be located"
)),
}
}

/// Builds a specific task
fn build_task(cfg: &PackageConfig, name: &str) -> Result<()> {
// Use relocatable linker script for this build
Expand Down Expand Up @@ -2004,20 +2104,38 @@ fn build(
);
output
});
cmd.env(
"RUSTFLAGS",
format!(
"-C link-arg=-z -C link-arg=common-page-size=0x20 \
-C link-arg=-z -C link-arg=max-page-size=0x20 \
-C llvm-args=--enable-machine-outliner=never \
-Z emit-stack-sizes \
-C overflow-checks=y \
-C metadata={} \
{}
",
cfg.link_script_hash, remap_path_prefix,
),
);

if version_meta().unwrap().channel == Channel::Nightly {
cmd.env(
"RUSTFLAGS",
format!(
"-C link-arg=-z -C link-arg=common-page-size=0x20 \
-C link-arg=-z -C link-arg=max-page-size=0x20 \
-C llvm-args=--enable-machine-outliner=never \
-Z emit-stack-sizes \
-C overflow-checks=y \
-C metadata={} \
{}
",
cfg.link_script_hash, remap_path_prefix,
),
);
} else {
cmd.env(
"RUSTFLAGS",
format!(
"-C link-arg=-z -C link-arg=common-page-size=0x20 \
-C link-arg=-z -C link-arg=max-page-size=0x20 \
-C llvm-args=--enable-machine-outliner=never \
-C overflow-checks=y \
-C metadata={} \
{}
",
cfg.link_script_hash, remap_path_prefix,
),
);
}

cmd.arg("--");

// We use attributes to conditionally import based on feature flags;
Expand Down
6 changes: 5 additions & 1 deletion drv/lpc55-swd/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ build-lpc55pins = { path = "../../build/lpc55pins" }
build-util = { path = "../../build/util" }
call_rustfmt = { path = "../../build/call_rustfmt" }
endoscope-abi = { path = "../../lib/endoscope-abi" }
endoscope = { path = "../../lib/endoscope", artifact="bin:endoscope", target = "thumbv7em-none-eabihf", features = ["soc_stm32h753"]}
# "artifact dependencies" (i.e. `artifact="bin:endoscope"` et al.) are not supported in stable. As
# such, building endoscope is done directly by the `dist` xtask when building this crate. See the
# `build_endoscope` function in `build/xtask/src/dist.rs`.
# For more, see https://github.com/rust-lang/cargo/issues/9096
# endoscope = { path = "../../lib/endoscope", artifact="bin:endoscope", target = "thumbv7em-none-eabihf", features = ["soc_stm32h753"]}
goblin = { workspace = true }
idol = { workspace = true }
quote = { workspace = true }
Expand Down
Loading