Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: validate build target for AWS Lambda, and check target component in toolchain #39

Merged
merged 14 commits into from Mar 31, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Expand Up @@ -24,6 +24,7 @@ cargo-zigbuild = "0.7.0"
cargo_metadata = "0.14.2"
clap = { version = "3.1.5", features = ["cargo", "derive"] }
dirs = "4.0.0"
home = "0.5.3"
http-api-problem = { version = "0.51.0", features = ["api-error", "hyper"] }
indicatif = "0.16.2"
inquire = "0.2.1"
Expand Down
41 changes: 37 additions & 4 deletions src/build.rs
@@ -1,4 +1,4 @@
use crate::{metadata, zig};
use crate::{metadata, toolchain, zig};
use cargo_zigbuild::Build as ZigBuild;
use clap::{Args, ValueHint};
use miette::{IntoDiagnostic, Result, WrapErr};
Expand Down Expand Up @@ -32,11 +32,18 @@ impl Build {
pub fn run(&mut self) -> Result<()> {
let rustc_meta = rustc_version::version_meta().into_diagnostic()?;
let host_target = &rustc_meta.host;
let release_channel = &rustc_meta.channel;

let build_target = self.build.target.get(0);
match build_target {
// Same explicit target as host target
Some(target) if host_target == target => self.build.disable_zig_linker = true,
Some(target) => {
// Validate that the build target is supported in AWS Lambda
check_build_target(target)?;
// Same explicit target as host target
if host_target == target {
self.build.disable_zig_linker = true
}
}
// No explicit target, but build host same as target host
None if host_target == "aarch64-unknown-linux-gnu"
|| host_target == "x86_64-unknown-linux-gnu" =>
Expand All @@ -49,7 +56,6 @@ impl Build {
None => {
self.build.target = vec!["x86_64-unknown-linux-gnu".into()];
}
_ => {}
}

let manifest_path = self
Expand Down Expand Up @@ -108,6 +114,14 @@ impl Build {
None => "debug",
};

// confirm that target component is included in host toolchain, or add
// it with `rustup` otherwise.
toolchain::check_target_component_with_rustc_meta(
rnag marked this conversation as resolved.
Show resolved Hide resolved
final_target,
host_target,
release_channel,
)?;

let target_dir = Path::new("target");
let lambda_dir = if let Some(dir) = &self.lambda_dir {
dir.clone()
Expand Down Expand Up @@ -145,3 +159,22 @@ impl Build {
Ok(())
}
}

/// Validate that the build target is supported in AWS Lambda
///
/// Here we use *starts with* instead of an exact match because:
/// - the target could also also be a *musl* variant: `x86_64-unknown-linux-musl`
/// - the target could also [specify a glibc version], which `cargo-zigbuild` supports
///
/// [specify a glibc version]: https://github.com/messense/cargo-zigbuild#specify-glibc-version
fn check_build_target(target: &str) -> Result<()> {
if !target.starts_with("aarch64-unknown-linux") && !target.starts_with("x86_64-unknown-linux") {
// Unsupported target for an AWS Lambda environment
return Err(miette::miette!(
"Invalid or unsupported target for AWS Lambda: {}",
target
));
}

Ok(())
}
1 change: 1 addition & 0 deletions src/main.rs
Expand Up @@ -8,6 +8,7 @@ mod invoke;
mod metadata;
mod progress;
mod start;
mod toolchain;
mod zig;

#[derive(Parser)]
Expand Down
6 changes: 3 additions & 3 deletions src/progress.rs
Expand Up @@ -6,11 +6,11 @@ pub(crate) struct Progress {
}

impl Progress {
pub fn start(msg: &str) -> Progress {
pub fn start(msg: impl ToString) -> Progress {
let bar = if is(atty::Stream::Stdout) {
Some(show_progress(msg))
} else {
println!("▹▹▹▹▹ {}", msg);
println!("▹▹▹▹▹ {}", msg.to_string());
None
};
Progress { bar }
Expand All @@ -25,7 +25,7 @@ impl Progress {
}
}

fn show_progress(msg: &str) -> ProgressBar {
fn show_progress(msg: impl ToString) -> ProgressBar {
let pb = ProgressBar::new_spinner();
pb.enable_steady_tick(120);
pb.set_style(
Expand Down
110 changes: 110 additions & 0 deletions src/toolchain.rs
@@ -0,0 +1,110 @@
use std::env;
use std::ffi::OsString;
use std::process::{Command, Stdio};

use miette::{IntoDiagnostic, Result, WrapErr};
use rustc_version::Channel;

/// Check if the target component is installed in the host toolchain, and add
/// it with `rustup` as needed.
///
/// # Note
/// This function calls `rustc -vV` to retrieve the host triple and the release
/// channel name.
#[allow(unused)]
pub fn check_target_component(component: &str) -> Result<()> {
let rustc_meta = rustc_version::version_meta().into_diagnostic()?;
let host_target = &rustc_meta.host;
let release_channel = &rustc_meta.channel;

check_target_component_with_rustc_meta(component, host_target, release_channel)
}

/// Check if the target component is installed in the host toolchain, and add
/// it with `rustup` as needed.
pub fn check_target_component_with_rustc_meta(
component: &str,
host: &str,
channel: &Channel,
) -> Result<()> {
// resolve $RUSTUP_HOME, which points to the `rustup` base directory
// https://rust-lang.github.io/rustup/environment-variables.html#environment-variables
let rustup_home = home::rustup_home().into_diagnostic()?;

// convert `Channel` enum to a lower-cased string representation
let channel_name = match channel {
Channel::Stable => "stable",
Channel::Nightly => "nightly",
Channel::Dev => "dev",
Channel::Beta => "beta",
};

// check if the target component is installed in the host toolchain
let target_component_is_added = rustup_home
.join("toolchains")
.join(format!("{channel_name}-{host}"))
.join("lib")
.join("rustlib")
.join(component)
.exists();

if !target_component_is_added {
// install target component using `rustup`
let pb = crate::progress::Progress::start(format_args!(
"Installing target component `{}`...",
component
));

let result = install_target_component(component);
let finish = if result.is_ok() {
"Target component installed"
} else {
"Failed to install target component"
};

pb.finish(finish);
}

Ok(())
}

/// Install target component in the host toolchain, using `rustup target add`
fn install_target_component(component: &str) -> Result<()> {
rnag marked this conversation as resolved.
Show resolved Hide resolved
let cmd = env::var_os("RUSTUP").unwrap_or_else(|| OsString::from("rustup"));

let mut child = Command::new(&cmd)
.args(&["target", "add", component])
.stderr(Stdio::null())
.stdout(Stdio::null())
.spawn()
.into_diagnostic()
.wrap_err_with(|| format!("Failed to run `{:?} target add {}`", cmd, component))?;

let status = child
.wait()
.into_diagnostic()
.wrap_err("Failed to wait on rustup process")?;
if !status.success() {
std::process::exit(status.code().unwrap_or(1));
}

Ok(())
}

#[cfg(test)]
mod tests {
use super::*;

/// Check target component is installed in the host toolchain, and install
/// it via `rustup target add` otherwise.
///
/// # Note
/// This test is marked as **ignored** so it doesn't add the target
/// component in a CI build.
#[test]
#[ignore]
fn test_check_target_component() -> crate::Result<()> {
let component = "aarch64-unknown-linux-gnu";
check_target_component(component)
}
}