Repository: shuttle-hq/shuttle (Rust)

Issue:
Error deploying workspace with library crate.
I've created a very simple Discord bot with serenity to test Shuttle: https://github.com/casperstorm/discord-bot-einar/tree/workspace-lib-crates.
Running `cargo shuttle run` works just fine:

```
Starting discord-bot-einar on http://127.0.0.1:8000
2022-10-20T15:01:15.904846000Z  INFO serenity::client::bridge::gateway::shard_manager: Telling shard queuer to start shard 0
2022-10-20T15:01:16.149037000Z  INFO serenity::client::bridge::gateway::shard_runner: [ShardRunner [0, 1]] Running
2022-10-20T15:01:16.406885000Z  INFO discord_bot_einar: BOT Einar is connected
```

However when running `cargo shuttle deploy` i get the following error:

> {error="Build error: failed to select a version for the requirement `data = \"^0.1.0\"`\ncandidate versions found which didn't match: 0.0.0\nlocation searched: crates.io index\nrequired by package `discord-bot-einar-c1fabb48-fb8c-4b83-9977-ad647b5ecea9 v0.1.0 (/opt/shuttle/shuttle-builds/discord-bot-einar)`"}

Do i have to do something different with my current structure of the bot? The structure is as follows:

```
├── Cargo.lock
├── Cargo.toml
├── data
│   ├── Cargo.toml
│   └── src
│       ├── data_time.rs
│       ├── lib.rs
│       └── rss.rs
└── src
      ├── lib.rs
      ├── rss.rs
      └── settings.rs
```


Discussion: I got a answer from [Shuttle Discord](https://discord.gg/HBan6PGG).

> Currently we use cargo package (https://doc.rust-lang.org/cargo/commands/cargo-package.html) to package users crates before sending them to the build server. This means that all crates used must be indexed on crates.io, so depending on an unpublished crate from a workspace won't work (neither will git dependencies).
This is something we want to support, but haven't been able to solve yet.
This should work after #767 



Fix (patch):
diff --git a/cargo-shuttle/src/lib.rs b/cargo-shuttle/src/lib.rs
index 4a4807c94e..c8cbe07c29 100644
--- a/cargo-shuttle/src/lib.rs
+++ b/cargo-shuttle/src/lib.rs
@@ -37,7 +37,7 @@ use git2::{Repository, StatusOptions};
 use ignore::overrides::OverrideBuilder;
 use ignore::WalkBuilder;
 use shuttle_common::models::{project, secret};
-use shuttle_service::builder::{build_crate, Runtime};
+use shuttle_service::builder::{build_workspace, Runtime};
 use std::fmt::Write;
 use strum::IntoEnumIterator;
 use tar::Builder;
@@ -448,7 +448,7 @@ impl Shuttle {
             working_directory.display()
         );
 
-        let runtime = build_crate(working_directory, run_args.release, tx).await?;
+        let runtimes = build_workspace(working_directory, run_args.release, tx).await?;
 
         trace!("loading secrets");
 
@@ -473,7 +473,7 @@ impl Shuttle {
 
         let service_name = self.ctx.project_name().to_string();
 
-        let (is_wasm, executable_path) = match runtime {
+        let (is_wasm, executable_path) = match runtimes[0].clone() {
             Runtime::Next(path) => (true, path),
             Runtime::Alpha(path) => (false, path),
         };
diff --git a/deployer/src/deployment/queue.rs b/deployer/src/deployment/queue.rs
index 3d0ebc106d..fb25caca7e 100644
--- a/deployer/src/deployment/queue.rs
+++ b/deployer/src/deployment/queue.rs
@@ -12,7 +12,7 @@ use crossbeam_channel::Sender;
 use opentelemetry::global;
 use serde_json::json;
 use shuttle_common::claims::Claim;
-use shuttle_service::builder::{build_crate, get_config, Runtime};
+use shuttle_service::builder::{build_workspace, get_config, Runtime};
 use tokio::time::{sleep, timeout};
 use tracing::{debug, debug_span, error, info, instrument, trace, warn, Instrument, Span};
 use tracing_opentelemetry::OpenTelemetrySpanExt;
@@ -27,7 +27,7 @@ use std::time::Duration;
 
 use cargo::core::compiler::{CompileMode, MessageFormat};
 use cargo::core::Workspace;
-use cargo::ops::{CompileOptions, TestOptions};
+use cargo::ops::{self, CompileOptions, TestOptions};
 use flate2::read::GzDecoder;
 use tar::Archive;
 use tokio::fs;
@@ -332,9 +332,11 @@ async fn build_deployment(
     project_path: &Path,
     tx: crossbeam_channel::Sender<Message>,
 ) -> Result<Runtime> {
-    build_crate(project_path, true, tx)
+    let runtimes = build_workspace(project_path, true, tx)
         .await
-        .map_err(|e| Error::Build(e.into()))
+        .map_err(|e| Error::Build(e.into()))?;
+
+    Ok(runtimes[0].clone())
 }
 
 #[instrument(skip(project_path, tx))]
@@ -382,13 +384,15 @@ async fn run_pre_deploy_tests(
     // Build tests with a maximum of 4 workers.
     compile_opts.build_config.jobs = 4;
 
+    compile_opts.spec = ops::Packages::All;
+
     let opts = TestOptions {
         compile_opts,
         no_run: false,
         no_fail_fast: false,
     };
 
-    cargo::ops::run_tests(&ws, &opts, &[]).map_err(TestError::Failed)
+    ops::run_tests(&ws, &opts, &[]).map_err(TestError::Failed)
 }
 
 /// This will store the path to the executable for each runtime, which will be the users project with
diff --git a/service/src/builder.rs b/service/src/builder.rs
index 5c52651301..fbd068a374 100644
--- a/service/src/builder.rs
+++ b/service/src/builder.rs
@@ -2,8 +2,8 @@ use std::path::{Path, PathBuf};
 
 use anyhow::{anyhow, bail, Context};
 use cargo::core::compiler::{CompileKind, CompileMode, CompileTarget, MessageFormat};
-use cargo::core::{Manifest, Shell, Summary, Verbosity, Workspace};
-use cargo::ops::{clean, compile, CleanOptions, CompileOptions};
+use cargo::core::{Package, Shell, Verbosity, Workspace};
+use cargo::ops::{self, clean, compile, CleanOptions, CompileOptions};
 use cargo::util::homedir;
 use cargo::util::interning::InternedString;
 use cargo::Config;
@@ -12,8 +12,9 @@ use crossbeam_channel::Sender;
 use pipe::PipeWriter;
 use tracing::{error, trace};
 
-use crate::NEXT_NAME;
+use crate::{NEXT_NAME, RUNTIME_NAME};
 
+#[derive(Clone, Debug, Eq, PartialEq)]
 /// How to run/build the project
 pub enum Runtime {
     Next(PathBuf),
@@ -21,11 +22,11 @@ pub enum Runtime {
 }
 
 /// Given a project directory path, builds the crate
-pub async fn build_crate(
+pub async fn build_workspace(
     project_path: &Path,
     release_mode: bool,
     tx: Sender<Message>,
-) -> anyhow::Result<Runtime> {
+) -> anyhow::Result<Vec<Runtime>> {
     let (read, write) = pipe::pipe();
     let project_path = project_path.to_owned();
 
@@ -49,29 +50,51 @@ pub async fn build_crate(
 
     let config = get_config(write)?;
     let manifest_path = project_path.join("Cargo.toml");
-    let mut ws = Workspace::new(&manifest_path, &config)?;
+    let ws = Workspace::new(&manifest_path, &config)?;
+    check_no_panic(&ws)?;
 
-    let current = ws.current_mut().map_err(|_| anyhow!("A Shuttle project cannot have a virtual manifest file - please ensure the `package` table is present in your Cargo.toml file."))?;
+    let mut alpha_packages = Vec::new();
+    let mut next_packages = Vec::new();
 
-    let summary = current.manifest_mut().summary_mut();
-    let is_next = is_next(summary);
+    for member in ws.members() {
+        if is_next(member) {
+            ensure_cdylib(member)?;
+            next_packages.push(member.name().to_string());
+        } else if is_alpha(member) {
+            ensure_binary(member)?;
+            alpha_packages.push(member.name().to_string());
+        }
+    }
 
-    if !is_next {
-        ensure_binary(current.manifest())?;
-    } else {
-        ensure_cdylib(current.manifest_mut())?;
+    let mut runtimes = Vec::new();
+
+    if !alpha_packages.is_empty() {
+        let opts = get_compile_options(&config, alpha_packages, release_mode, false)?;
+        let compilation = compile(&ws, &opts)?;
+
+        let mut alpha_binaries = compilation
+            .binaries
+            .iter()
+            .map(|binary| Runtime::Alpha(binary.path.clone()))
+            .collect();
+
+        runtimes.append(&mut alpha_binaries);
     }
 
-    check_no_panic(&ws)?;
+    if !next_packages.is_empty() {
+        let opts = get_compile_options(&config, next_packages, release_mode, true)?;
+        let compilation = compile(&ws, &opts)?;
 
-    let opts = get_compile_options(&config, release_mode, is_next)?;
-    let compilation = compile(&ws, &opts)?;
+        let mut next_libraries = compilation
+            .cdylibs
+            .iter()
+            .map(|binary| Runtime::Next(binary.path.clone()))
+            .collect();
 
-    Ok(if is_next {
-        Runtime::Next(compilation.cdylibs[0].path.clone())
-    } else {
-        Runtime::Alpha(compilation.binaries[0].path.clone())
-    })
+        runtimes.append(&mut next_libraries);
+    }
+
+    Ok(runtimes)
 }
 
 pub fn clean_crate(project_path: &Path, release_mode: bool) -> anyhow::Result<Vec<String>> {
@@ -138,6 +161,7 @@ pub fn get_config(writer: PipeWriter) -> anyhow::Result<Config> {
 /// Get options to compile in build mode
 fn get_compile_options(
     config: &Config,
+    packages: Vec<String>,
     release_mode: bool,
     wasm: bool,
 ) -> anyhow::Result<CompileOptions> {
@@ -166,19 +190,28 @@ fn get_compile_options(
         CompileKind::Host
     }];
 
+    opts.spec = ops::Packages::Packages(packages);
+
     Ok(opts)
 }
 
-fn is_next(summary: &Summary) -> bool {
-    summary
+fn is_next(package: &Package) -> bool {
+    package
         .dependencies()
         .iter()
         .any(|dependency| dependency.package_name() == NEXT_NAME)
 }
 
+fn is_alpha(package: &Package) -> bool {
+    package
+        .dependencies()
+        .iter()
+        .any(|dependency| dependency.package_name() == RUNTIME_NAME)
+}
+
 /// Make sure the project is a binary for alpha projects.
-fn ensure_binary(manifest: &Manifest) -> anyhow::Result<()> {
-    if manifest.targets().iter().any(|target| target.is_bin()) {
+fn ensure_binary(package: &Package) -> anyhow::Result<()> {
+    if package.targets().iter().any(|target| target.is_bin()) {
         Ok(())
     } else {
         bail!("Your Shuttle project must be a binary.")
@@ -186,24 +219,11 @@ fn ensure_binary(manifest: &Manifest) -> anyhow::Result<()> {
 }
 
 /// Make sure "cdylib" is set for shuttle-next projects, else set it if possible.
-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(),
-            );
-        }
-
+fn ensure_cdylib(package: &Package) -> anyhow::Result<()> {
+    if package.targets().iter().any(|target| target.is_lib()) {
         Ok(())
     } else {
-        bail!("Your Shuttle project must be a library. Please add `[lib]` to your Cargo.toml file.")
+        bail!("Your Shuttle next project must be a library. Please add `[lib]` to your Cargo.toml file.")
     }
 }
 
diff --git a/service/src/lib.rs b/service/src/lib.rs
index f0b98f0e4e..50cbccfce5 100644
--- a/service/src/lib.rs
+++ b/service/src/lib.rs
@@ -190,3 +190,4 @@ pub trait Service: Send {
 }
 
 pub const NEXT_NAME: &str = "shuttle-next";
+pub const RUNTIME_NAME: &str = "shuttle-runtime";