Skip to content
Merged
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
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,11 @@ jobs:
- name: Test (workshop, concurrent via nextest)
run: cargo nextest run --locked -p workshop -p workshop-server

# nextest does not run doctests; the workspace doctest step in the
# `test` job excludes both workshop crates, so cover them here.
- name: Doctests (workshop)
run: cargo test --doc -p workshop -p workshop-server

- name: Test Gateway process ownership races
run: |
cargo test --locked -p shared-sidecar a_process_lifetime_lease_recovers_after_its_owner_is_terminated
Expand Down Expand Up @@ -379,3 +384,24 @@ jobs:

- name: cargo audit
run: cargo audit

# Single required status check for branch protection: it fails when any
# job fails or is cancelled, so a cancelled job cannot read as green.
# needs: is a static list, so every newly added job must be added to it
# or that job bypasses this gate.
ci-green:
runs-on: ubuntu-latest
needs: [fmt, clippy, test, docs, check-workshop, check-workshop-linux, ui, msrv, supply-chain]
if: always()
steps:
- name: Verify every job succeeded
shell: bash
run: |
status=0
for result in ${{ join(needs.*.result, ' ') }}; do
if [ "$result" != "success" ]; then
echo "a required job did not succeed: $result"
status=1
fi
done
exit $status
7 changes: 6 additions & 1 deletion Cargo.lock

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

16 changes: 8 additions & 8 deletions crates/build-llama-cuda/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ OPTIONS:

/// Parses the command line into a [`BuildRequest`]. Every error exit
/// prints the usage text.
fn parse_args(args: &[String]) -> Result<BuildRequest, String> {
fn parse_args(args: &[String]) -> anyhow::Result<BuildRequest> {
let mut source: Option<PathBuf> = None;
let mut tag: Option<String> = None;
let mut out: Option<PathBuf> = None;
Expand All @@ -44,7 +44,7 @@ fn parse_args(args: &[String]) -> Result<BuildRequest, String> {
iter.next()
.filter(|value| !value.starts_with("--"))
.cloned()
.ok_or_else(|| format!("{arg} needs a value\n\n{USAGE}"))
.ok_or_else(|| anyhow::anyhow!("{arg} needs a value\n\n{USAGE}"))
};
match arg.as_str() {
"--source" => source = Some(PathBuf::from(value(&mut iter)?)),
Expand All @@ -56,23 +56,23 @@ fn parse_args(args: &[String]) -> Result<BuildRequest, String> {
if entry.is_empty()
|| !entry.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
{
return Err(format!(
return Err(anyhow::anyhow!(
"malformed --arch entry `{entry}` (expected for example 120a-real)\n\n{USAGE}"
));
}
archs.push(entry.to_string());
}
}
"--no-smoke" => smoke = false,
"-h" | "--help" => return Err(USAGE.to_string()),
other => return Err(format!("unknown argument `{other}`\n\n{USAGE}")),
"-h" | "--help" => return Err(anyhow::anyhow!(USAGE.to_string())),
other => return Err(anyhow::anyhow!("unknown argument `{other}`\n\n{USAGE}")),
}
}

Ok(BuildRequest {
source: source.ok_or_else(|| format!("missing required --source\n\n{USAGE}"))?,
tag: tag.ok_or_else(|| format!("missing required --tag\n\n{USAGE}"))?,
out: out.ok_or_else(|| format!("missing required --out\n\n{USAGE}"))?,
source: source.ok_or_else(|| anyhow::anyhow!("missing required --source\n\n{USAGE}"))?,
tag: tag.ok_or_else(|| anyhow::anyhow!("missing required --tag\n\n{USAGE}"))?,
out: out.ok_or_else(|| anyhow::anyhow!("missing required --out\n\n{USAGE}"))?,
archs,
smoke,
})
Expand Down
1 change: 1 addition & 0 deletions crates/build-ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ publish = false
description = "Build-script helper that bundles a crate's ui/ sources with esbuild into OUT_DIR"

[dependencies]
anyhow.workspace = true

[lints]
workspace = true
39 changes: 22 additions & 17 deletions crates/build-ui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,18 @@ pub struct UiBuild {
/// bundle.
///
/// # Errors
/// Returns an error string when not run through Cargo, when the local
/// Returns an error when not run through Cargo, when the local
/// esbuild install is missing or fails, or when a static file cannot be
/// copied.
pub fn build(config: UiBuild) -> Result<(), String> {
pub fn build(config: UiBuild) -> anyhow::Result<()> {
let manifest_dir = PathBuf::from(
std::env::var_os("CARGO_MANIFEST_DIR")
.ok_or("CARGO_MANIFEST_DIR is not set; run through cargo")?,
.ok_or_else(|| anyhow::anyhow!("CARGO_MANIFEST_DIR is not set; run through cargo"))?,
);
let out_dir = PathBuf::from(
std::env::var_os("OUT_DIR")
.ok_or_else(|| anyhow::anyhow!("OUT_DIR is not set; run through cargo"))?,
);
let out_dir =
PathBuf::from(std::env::var_os("OUT_DIR").ok_or("OUT_DIR is not set; run through cargo")?);
let ui_dir = manifest_dir.join("ui");
let dist_dir = out_dir.join("ui-dist");

Expand All @@ -63,7 +65,7 @@ pub fn build(config: UiBuild) -> Result<(), String> {
// linger into what debug builds serve and release builds embed.
if dist_dir.exists() {
std::fs::remove_dir_all(&dist_dir)
.map_err(|error| format!("clear {}: {error}", dist_dir.display()))?;
.map_err(|error| anyhow::anyhow!("clear {}: {error}", dist_dir.display()))?;
}
bundle(&ui_dir, &dist_dir, config.define_app_version)?;
copy_static(&ui_dir, &dist_dir, config.static_files)?;
Expand Down Expand Up @@ -101,7 +103,7 @@ fn watch(ui_dir: &Path, config: &UiBuild) {
/// Runs the esbuild bundle step from the local `ui/node_modules` install.
/// There is no `npx` fallback: `npx` can download a different esbuild
/// version and produce different output.
fn bundle(ui_dir: &Path, dist_dir: &Path, define_app_version: bool) -> Result<(), String> {
fn bundle(ui_dir: &Path, dist_dir: &Path, define_app_version: bool) -> anyhow::Result<()> {
let mut command = esbuild_command(ui_dir)?;
command.current_dir(ui_dir).args([
"src/main.ts",
Expand All @@ -114,20 +116,23 @@ fn bundle(ui_dir: &Path, dist_dir: &Path, define_app_version: bool) -> Result<()
command.arg("--minify");
}
if define_app_version {
let version = std::env::var("CARGO_PKG_VERSION")
.map_err(|error| format!("CARGO_PKG_VERSION is not set: {error}; run through cargo"))?;
let version = std::env::var("CARGO_PKG_VERSION").map_err(|error| {
anyhow::anyhow!("CARGO_PKG_VERSION is not set: {error}; run through cargo")
})?;
// Single quotes: esbuild evaluates the define value as a JS string
// literal, and unlike double quotes they pass through `cmd /c` on
// Windows untouched.
command.arg(format!("--define:__APP_VERSION__='{version}'"));
}
let output = command.output().map_err(|error| {
format!("esbuild could not be started: {error}; install Node.js 22 so it is on PATH")
anyhow::anyhow!(
"esbuild could not be started: {error}; install Node.js 22 so it is on PATH"
)
})?;
if output.status.success() {
return Ok(());
}
Err(format!(
Err(anyhow::anyhow!(
"the UI bundle failed (status {}):\n{}\n{}",
output.status,
String::from_utf8_lossy(&output.stdout),
Expand All @@ -138,7 +143,7 @@ fn bundle(ui_dir: &Path, dist_dir: &Path, define_app_version: bool) -> Result<()
/// Builds the command that invokes the local esbuild install, failing with
/// the setup instructions when `ui/node_modules` is absent. On Windows the
/// npm shim is a `.cmd` file, which only runs through `cmd /c`.
fn esbuild_command(ui_dir: &Path) -> Result<Command, String> {
fn esbuild_command(ui_dir: &Path) -> anyhow::Result<Command> {
let bin_dir = ui_dir.join("node_modules").join(".bin");

#[cfg(windows)]
Expand All @@ -159,25 +164,25 @@ fn esbuild_command(ui_dir: &Path) -> Result<Command, String> {
}
}

Err(format!(
Err(anyhow::anyhow!(
"ui/node_modules is missing; run `npm ci` in {} first",
ui_dir.display()
))
}

/// Copies the static UI files next to the bundle, keeping the relative
/// paths.
fn copy_static(ui_dir: &Path, dist_dir: &Path, static_files: &[&str]) -> Result<(), String> {
fn copy_static(ui_dir: &Path, dist_dir: &Path, static_files: &[&str]) -> anyhow::Result<()> {
std::fs::create_dir_all(dist_dir)
.map_err(|error| format!("create {}: {error}", dist_dir.display()))?;
.map_err(|error| anyhow::anyhow!("create {}: {error}", dist_dir.display()))?;
for file in static_files {
let target = dist_dir.join(file);
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent)
.map_err(|error| format!("create the parent for {file}: {error}"))?;
.map_err(|error| anyhow::anyhow!("create the parent for {file}: {error}"))?;
}
std::fs::copy(ui_dir.join(file), &target)
.map_err(|error| format!("copy ui/{file} into the bundle output: {error}"))?;
.map_err(|error| anyhow::anyhow!("copy ui/{file} into the bundle output: {error}"))?;
}
Ok(())
}
1 change: 1 addition & 0 deletions crates/build-workshop/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ name = "build-workshop"
path = "src/main.rs"

[dependencies]
anyhow.workspace = true
ctrlc.workspace = true

[dev-dependencies]
Expand Down
44 changes: 27 additions & 17 deletions crates/build-workshop/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,18 +158,22 @@ impl InterruptController {
}
}

static PROCESS_INTERRUPT: OnceLock<Result<InterruptController, String>> = OnceLock::new();
static PROCESS_INTERRUPT: OnceLock<Result<InterruptController, Arc<anyhow::Error>>> =
OnceLock::new();

fn install_interrupt_handler() -> Result<InterruptController, String> {
fn install_interrupt_handler() -> Result<InterruptController, Arc<anyhow::Error>> {
match PROCESS_INTERRUPT.get_or_init(|| {
let interrupt = InterruptController::isolated();
let handler_interrupt = interrupt.clone();
ctrlc::set_handler(move || handler_interrupt.request())
.map_err(|error| format!("cannot install the interrupt handler: {error}"))?;
ctrlc::set_handler(move || handler_interrupt.request()).map_err(|error| {
Arc::new(anyhow::anyhow!(
"cannot install the interrupt handler: {error}"
))
})?;
Ok(interrupt)
}) {
Ok(interrupt) => Ok(interrupt.clone()),
Err(error) => Err(error.clone()),
Err(error) => Err(Arc::clone(error)),
}
}

Expand Down Expand Up @@ -289,29 +293,31 @@ struct BuildEnvironment {
}

impl BuildEnvironment {
fn discover() -> Result<Self, String> {
fn discover() -> Result<Self, anyhow::Error> {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let workspace_root = manifest_dir
.parent()
.and_then(|crates| crates.parent())
.ok_or_else(|| {
format!(
anyhow::anyhow!(
"cannot derive the workspace root from {}",
manifest_dir.display()
)
})?
.to_path_buf();
let target_root = match std::env::var_os("CARGO_TARGET_DIR") {
Some(value) if value.is_empty() => {
return Err("CARGO_TARGET_DIR must not be empty".to_owned());
return Err(anyhow::anyhow!("CARGO_TARGET_DIR must not be empty"));
}
Some(value) => {
let path = PathBuf::from(value);
if path.is_absolute() {
path
} else {
std::env::current_dir()
.map_err(|error| format!("cannot read the current directory: {error}"))?
.map_err(|error| {
anyhow::anyhow!("cannot read the current directory: {error}")
})?
.join(path)
}
}
Expand All @@ -321,7 +327,7 @@ impl BuildEnvironment {
.or_else(|| option_env!("CARGO").map(OsString::from))
.map(PathBuf::from)
.ok_or_else(|| {
"Cargo did not provide the executable used for this command".to_owned()
anyhow::anyhow!("Cargo did not provide the executable used for this command")
})?;
Ok(Self {
workspace_root,
Expand Down Expand Up @@ -355,38 +361,40 @@ impl fmt::Display for BuildError {
}
}

fn parse_arguments(args: &[String]) -> Result<BuildRequest, String> {
fn parse_arguments(args: &[String]) -> Result<BuildRequest, anyhow::Error> {
let mut profile = Profile::Debug;
let mut release_seen = false;
let mut target = None;
let mut index = 0;
while index < args.len() {
match args[index].as_str() {
"--release" if release_seen => {
return Err(format!("duplicate argument `--release`\n\n{USAGE}"));
return Err(anyhow::anyhow!("duplicate argument `--release`\n\n{USAGE}"));
}
"--release" => {
profile = Profile::Release;
release_seen = true;
index += 1;
}
"--target" if target.is_some() => {
return Err(format!("duplicate argument `--target`\n\n{USAGE}"));
return Err(anyhow::anyhow!("duplicate argument `--target`\n\n{USAGE}"));
}
"--target" => {
let value = args.get(index + 1).ok_or_else(|| {
format!("argument `--target` needs a target triple\n\n{USAGE}")
anyhow::anyhow!("argument `--target` needs a target triple\n\n{USAGE}")
})?;
if value.starts_with('-') || !valid_target_triple(value) {
return Err(format!(
return Err(anyhow::anyhow!(
"argument `--target` needs a valid target triple, got `{value}`\n\n{USAGE}"
));
}
target = Some(value.clone());
index += 2;
}
argument => {
return Err(format!("unsupported argument `{argument}`\n\n{USAGE}"));
return Err(anyhow::anyhow!(
"unsupported argument `{argument}`\n\n{USAGE}"
));
}
}
}
Expand Down Expand Up @@ -837,7 +845,9 @@ mod tests {
vec!["--profile".to_owned(), "dist".to_owned()],
vec!["gateway".to_owned()],
] {
let error = parse_arguments(&args).expect_err("unsupported argument");
let error = parse_arguments(&args)
.expect_err("unsupported argument")
.to_string();
assert!(error.contains("unsupported argument"), "{error}");
assert!(error.contains(USAGE), "{error}");
}
Expand Down
Loading
Loading