use std::any::Any;
use std::ffi::OsStr;
use std::net::SocketAddr;
use std::panic::AssertUnwindSafe;
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Context};
use cargo::core::compiler::{CompileMode, MessageFormat};
use cargo::core::{Manifest, PackageId, Shell, Summary, Verbosity, Workspace};
use cargo::ops::{compile, CompileOptions};
use cargo::util::interning::InternedString;
use cargo::util::{homedir, ToSemver};
use cargo::Config;
use cargo_metadata::Message;
use crossbeam_channel::Sender;
use libloading::{Library, Symbol};
use pipe::PipeWriter;
use thiserror::Error as ThisError;
use tracing::{error, trace};
use futures::FutureExt;
use uuid::Uuid;
use crate::error::CustomError;
use crate::{logger, Bootstrapper, NAME, VERSION};
use crate::{Error, Factory, ServeHandle};
const ENTRYPOINT_SYMBOL_NAME: &[u8] = b"_create_service\0";
type CreateService = unsafe extern "C" fn() -> *mut Bootstrapper;
#[derive(Debug, ThisError)]
pub enum LoaderError {
#[error("failed to load library: {0}")]
Load(libloading::Error),
#[error("failed to find the shuttle entrypoint. Did you use the provided shuttle macros?")]
GetEntrypoint(libloading::Error),
}
pub type LoadedService = (ServeHandle, Library);
pub struct Loader {
bootstrapper: Bootstrapper,
so: Library,
}
impl Loader {
pub fn from_so_file<P: AsRef<OsStr>>(so_path: P) -> Result<Self, LoaderError> {
trace!("loading {:?}", so_path.as_ref().to_str());
unsafe {
let lib = Library::new(so_path).map_err(LoaderError::Load)?;
let entrypoint: Symbol<CreateService> = lib
.get(ENTRYPOINT_SYMBOL_NAME)
.map_err(LoaderError::GetEntrypoint)?;
let raw = entrypoint();
Ok(Self {
bootstrapper: *Box::from_raw(raw),
so: lib,
})
}
}
pub async fn load(
self,
factory: &mut dyn Factory,
addr: SocketAddr,
logger: logger::Logger,
) -> Result<LoadedService, Error> {
let mut bootstrapper = self.bootstrapper;
AssertUnwindSafe(bootstrapper.bootstrap(factory, logger))
.catch_unwind()
.await
.map_err(|e| Error::BuildPanic(map_any_to_panic_string(e)))??;
trace!("bootstrapping done");
let handle = tokio::spawn(async move {
bootstrapper.into_handle(addr)?.await.map_err(|e| {
if e.is_panic() {
let mes = e.into_panic();
Error::BindPanic(map_any_to_panic_string(mes))
} else {
Error::Custom(CustomError::new(e))
}
})?
});
trace!("creating handle done");
Ok((handle, self.so))
}
}
pub async fn build_crate(
deployment_id: Uuid,
project_path: &Path,
release_mode: bool,
tx: Sender<Message>,
) -> anyhow::Result<PathBuf> {
let (read, write) = pipe::pipe();
let project_path = project_path.to_owned();
let handle = tokio::spawn(async move {
trace!("started thread to build crate");
let config = get_config(write)?;
let manifest_path = project_path.join("Cargo.toml");
let mut ws = Workspace::new(&manifest_path, &config)?;
let current = ws.current_mut().map_err(|_| anyhow!("A Shuttle project cannot have a virtual manifest file - please ensure your Cargo.toml file specifies it as a library."))?;
let manifest = current.manifest_mut();
ensure_cdylib(manifest)?;
let summary = current.manifest_mut().summary_mut();
make_name_unique(summary, deployment_id);
check_version(summary)?;
check_no_panic(&ws)?;
let opts = get_compile_options(&config, release_mode)?;
let compilation = compile(&ws, &opts);
Ok(compilation?.cdylibs[0].path.clone())
});
tokio::spawn(async move {
trace!("started thread to to capture build output stream");
for message in Message::parse_stream(read) {
trace!(?message, "parsed cargo message");
match message {
Ok(message) => {
if let Err(error) = tx.send(message) {
error!("failed to send cargo message on channel: {error}");
}
}
Err(error) => {
error!("failed to parse cargo message: {error}");
}
}
}
});
handle.await?
}
pub fn get_config(writer: PipeWriter) -> anyhow::Result<Config> {
let mut shell = Shell::from_write(Box::new(writer));
shell.set_verbosity(Verbosity::Normal);
let cwd = std::env::current_dir()
.with_context(|| "couldn't get the current directory of the process")?;
let homedir = homedir(&cwd).ok_or_else(|| {
anyhow!(
"Cargo couldn't find your home directory. \
This probably means that $HOME was not set."
)
})?;
Ok(Config::new(shell, cwd, homedir))
}
fn get_compile_options(config: &Config, release_mode: bool) -> anyhow::Result<CompileOptions> {
let mut opts = CompileOptions::new(config, CompileMode::Build)?;
opts.build_config.message_format = MessageFormat::Json {
render_diagnostics: false,
short: false,
ansi: false,
};
opts.build_config.requested_profile = if release_mode {
InternedString::new("release")
} else {
InternedString::new("dev")
};
Ok(opts)
}
fn ensure_cdylib(manifest: &mut Manifest) -> anyhow::Result<()> {
if let Some(target) = manifest
.targets_mut()
.iter_mut()
.find(|target| target.is_lib())
{
if !target.is_cdylib() {
*target = cargo::core::manifest::Target::lib_target(
target.name(),
vec![cargo::core::compiler::CrateType::Cdylib],
target.src_path().path().unwrap().to_path_buf(),
target.edition(),
);
}
Ok(())
} else {
Err(anyhow!(
"Your Shuttle project must be a library. Please add `[lib]` to your Cargo.toml file."
))
}
}
fn make_name_unique(summary: &mut Summary, deployment_id: Uuid) {
let old_package_id = summary.package_id();
*summary = summary.clone().override_id(
PackageId::new(
format!("{}-{deployment_id}", old_package_id.name()),
old_package_id.version(),
old_package_id.source_id(),
)
.unwrap(),
);
}
fn check_version(summary: &Summary) -> anyhow::Result<()> {
let valid_version = VERSION.to_semver().unwrap();
let version_req = if let Some(shuttle) = summary
.dependencies()
.iter()
.find(|dependency| dependency.package_name() == NAME)
{
shuttle.version_req()
} else {
return Err(anyhow!("this crate does not use the shutte service"));
};
if version_req.matches(&valid_version) {
Ok(())
} else {
Err(anyhow!(
"the version of `shuttle-service` specified as a dependency to this service ({version_req}) is not supported by this project instance ({valid_version}); try updating `shuttle-service` to '{valid_version}' or update the project instance using `cargo shuttle project rm` and `cargo shuttle project new`"
))
}
}
fn check_no_panic(ws: &Workspace) -> anyhow::Result<()> {
if let Some(profiles) = ws.profiles() {
for profile in profiles.get_all().values() {
if profile.panic.as_deref() == Some("abort") {
return Err(anyhow!("Your Shuttle project cannot have panics that abort. Please ensure your Cargo.toml does not contain `panic = \"abort\"` for any profiles."));
}
}
}
Ok(())
}
fn map_any_to_panic_string(a: Box<dyn Any>) -> String {
a.downcast_ref::<&str>()
.map(|x| x.to_string())
.unwrap_or_else(|| "<no panic message>".to_string())
}
#[cfg(test)]
mod tests {
mod from_so_file {
use crate::loader::{Loader, LoaderError};
#[test]
fn invalid() {
let result = Loader::from_so_file("invalid.so");
assert!(matches!(result, Err(LoaderError::Load(_))));
}
}
}