Skip to content

Commit 03e25b6

Browse files
authored
refa(cargo-shuttle): find binary target based on macro in source (#2059)
* refa(cargo-shuttle): find binary target to build based on macro in source * add tests * nits * nit: clarity * nit * fix: don't require final ] when looking for macro
1 parent 875d934 commit 03e25b6

File tree

14 files changed

+173
-196
lines changed

14 files changed

+173
-196
lines changed

cargo-shuttle/src/builder.rs

Lines changed: 71 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,27 @@
1-
use std::fs::read_to_string;
1+
use std::fs;
22
use std::path::{Path, PathBuf};
33
use std::process::Stdio;
44

5-
use anyhow::{anyhow, bail, Context};
6-
use cargo_metadata::{Metadata, Package};
5+
use anyhow::{bail, Context, Result};
6+
use cargo_metadata::{Metadata, Package, Target};
77
use shuttle_common::constants::RUNTIME_NAME;
88
use tokio::io::AsyncBufReadExt;
9-
use tracing::{debug, error, info, trace};
9+
use tracing::{error, trace};
1010

11-
#[derive(Clone, Debug, Eq, PartialEq)]
1211
/// This represents a compiled Shuttle service
12+
#[derive(Clone, Debug, Eq, PartialEq)]
1313
pub struct BuiltService {
1414
pub workspace_path: PathBuf,
15-
pub manifest_path: PathBuf,
16-
pub package_name: String,
15+
pub target_name: String,
1716
pub executable_path: PathBuf,
1817
}
1918

20-
impl BuiltService {
21-
/// The directory that contains the crate (that Cargo.toml is in)
22-
pub fn crate_directory(&self) -> &Path {
23-
self.manifest_path
24-
.parent()
25-
.expect("manifest to be in a directory")
26-
}
27-
28-
/// Try to get the service name of a crate from Shuttle.toml in the crate root, if it doesn't
29-
/// exist get it from the Cargo.toml package name of the crate.
30-
pub fn service_name(&self) -> anyhow::Result<String> {
31-
let shuttle_toml_path = self.crate_directory().join("Shuttle.toml");
32-
33-
match extract_shuttle_toml_name(shuttle_toml_path) {
34-
Ok(service_name) => Ok(service_name),
35-
Err(error) => {
36-
debug!(?error, "failed to get service name from Shuttle.toml");
37-
38-
// Couldn't get name from Shuttle.toml, use package name instead.
39-
Ok(self.package_name.clone())
40-
}
41-
}
42-
}
43-
}
44-
45-
fn extract_shuttle_toml_name(path: PathBuf) -> anyhow::Result<String> {
46-
let shuttle_toml =
47-
read_to_string(path.as_path()).map_err(|_| anyhow!("{} not found", path.display()))?;
48-
49-
let toml: toml::Value =
50-
toml::from_str(&shuttle_toml).context("failed to parse Shuttle.toml")?;
51-
52-
let name = toml
53-
.get("name")
54-
.context("couldn't find `name` key in Shuttle.toml")?
55-
.as_str()
56-
.context("`name` key in Shuttle.toml must be a string")?
57-
.to_string();
58-
59-
Ok(name)
60-
}
61-
62-
/// Given a project directory path, builds the crate
19+
/// Builds Shuttle service in given project directory
6320
pub async fn build_workspace(
6421
project_path: &Path,
6522
release_mode: bool,
6623
tx: tokio::sync::mpsc::Sender<String>,
67-
deployment: bool,
68-
) -> anyhow::Result<Vec<BuiltService>> {
24+
) -> Result<BuiltService> {
6925
let project_path = project_path.to_owned();
7026
let manifest_path = project_path.join("Cargo.toml");
7127
if !manifest_path.exists() {
@@ -100,29 +56,23 @@ pub async fn build_workspace(
10056
notification.abort();
10157

10258
let metadata = async_cargo_metadata(manifest_path.as_path()).await?;
103-
let packages = find_shuttle_packages(&metadata)?;
104-
if packages.is_empty() {
105-
bail!(
106-
"Did not find any packages that Shuttle can run. \
107-
Make sure your crate has a binary target that uses `#[shuttle_runtime::main]`."
108-
);
109-
}
59+
let (package, target) = find_first_shuttle_package(&metadata)?;
11060

111-
let services = compile(
112-
packages,
61+
let service = cargo_build(
62+
package,
63+
target,
11364
release_mode,
11465
project_path.clone(),
11566
metadata.target_directory.clone(),
116-
deployment,
11767
tx.clone(),
11868
)
11969
.await?;
120-
trace!("packages compiled");
70+
trace!("package compiled");
12171

122-
Ok(services)
72+
Ok(service)
12373
}
12474

125-
pub async fn async_cargo_metadata(manifest_path: &Path) -> anyhow::Result<Metadata> {
75+
pub async fn async_cargo_metadata(manifest_path: &Path) -> Result<Metadata> {
12676
let metadata = {
12777
// Modified implementaion of `cargo_metadata::MetadataCommand::exec` (from v0.15.3).
12878
// Uses tokio Command instead of std, to make this operation non-blocking.
@@ -149,63 +99,62 @@ pub async fn async_cargo_metadata(manifest_path: &Path) -> anyhow::Result<Metada
14999
Ok(metadata)
150100
}
151101

152-
pub fn find_shuttle_packages(metadata: &Metadata) -> anyhow::Result<Vec<Package>> {
102+
/// Find crates with a runtime dependency and main macro
103+
fn find_shuttle_packages(metadata: &Metadata) -> Result<Vec<(Package, Target)>> {
153104
let mut packages = Vec::new();
105+
trace!("Finding Shuttle-related packages");
154106
for member in metadata.workspace_packages() {
155-
// skip non-Shuttle-related crates
156-
// (must have runtime dependency and not be just a library)
157107
let has_runtime_dep = member
158108
.dependencies
159109
.iter()
160110
.any(|dependency| dependency.name == RUNTIME_NAME);
161-
let has_binary_target = member.targets.iter().any(|t| t.is_bin());
162-
if !(has_runtime_dep && has_binary_target) {
111+
if !has_runtime_dep {
112+
trace!("Skipping {}, no shuttle-runtime dependency", member.name);
163113
continue;
164114
}
165115

166-
let mut shuttle_deps = member
167-
.dependencies
168-
.iter()
169-
.filter(|&d| d.name.starts_with("shuttle-"))
170-
.map(|d| format!("{} '{}'", d.name, d.req))
171-
.collect::<Vec<_>>();
172-
shuttle_deps.sort();
173-
info!(name = member.name, deps = ?shuttle_deps, "Found workspace member with shuttle dependencies");
174-
packages.push(member.to_owned());
116+
let mut target = None;
117+
for t in member.targets.iter() {
118+
if t.is_bin()
119+
&& fs::read_to_string(t.src_path.as_std_path())
120+
.context("reading to check for shuttle macro")?
121+
// don't look for the last ] so that we also find instances of `#[shuttle_runtime::main(...)]`
122+
.contains("#[shuttle_runtime::main")
123+
{
124+
target = Some(t);
125+
break;
126+
}
127+
}
128+
let Some(target) = target else {
129+
trace!(
130+
"Skipping {}, no binary target with a #[shuttle_runtime::main] macro",
131+
member.name
132+
);
133+
continue;
134+
};
135+
136+
trace!("Found {}", member.name);
137+
packages.push((member.to_owned(), target.to_owned()));
175138
}
176139

177140
Ok(packages)
178141
}
179142

180-
// Only used in deployer
181-
pub async fn clean_crate(project_path: &Path) -> anyhow::Result<()> {
182-
let manifest_path = project_path.join("Cargo.toml");
183-
if !manifest_path.exists() {
184-
bail!("Cargo manifest file not found: {}", manifest_path.display());
185-
}
186-
if !tokio::process::Command::new("cargo")
187-
.arg("clean")
188-
.arg("--manifest-path")
189-
.arg(&manifest_path)
190-
.arg("--offline")
191-
.status()
192-
.await?
193-
.success()
194-
{
195-
bail!("`cargo clean` failed. Did you build anything yet?");
196-
}
197-
198-
Ok(())
143+
pub fn find_first_shuttle_package(metadata: &Metadata) -> Result<(Package, Target)> {
144+
find_shuttle_packages(metadata)?.into_iter().next().context(
145+
"Expected at least one target that Shuttle can build. \
146+
Make sure your crate has a binary target that uses a fully qualified `#[shuttle_runtime::main]`.",
147+
)
199148
}
200149

201-
async fn compile(
202-
packages: Vec<Package>,
150+
async fn cargo_build(
151+
package: Package,
152+
target: Target,
203153
release_mode: bool,
204154
project_path: PathBuf,
205155
target_path: impl Into<PathBuf>,
206-
deployment: bool,
207156
tx: tokio::sync::mpsc::Sender<String>,
208-
) -> anyhow::Result<Vec<BuiltService>> {
157+
) -> Result<BuiltService> {
209158
let manifest_path = project_path.join("Cargo.toml");
210159
if !manifest_path.exists() {
211160
bail!("Cargo manifest file not found: {}", manifest_path.display());
@@ -221,17 +170,11 @@ async fn compile(
221170
.arg("--color=always") // piping disables auto color, but we want it
222171
.current_dir(project_path.as_path());
223172

224-
if deployment {
225-
cmd.arg("--jobs=4");
226-
}
227-
228-
// TODO: Compile only one binary target in the package.
229-
for package in &packages {
230-
if package.features.contains_key("shuttle") {
231-
cmd.arg("--no-default-features").arg("--features=shuttle");
232-
}
233-
cmd.arg("--package").arg(package.name.as_str());
173+
if package.features.contains_key("shuttle") {
174+
cmd.arg("--no-default-features").arg("--features=shuttle");
234175
}
176+
cmd.arg("--package").arg(package.name.as_str());
177+
cmd.arg("--bin").arg(target.name.as_str());
235178

236179
let profile = if release_mode {
237180
cmd.arg("--release");
@@ -255,30 +198,22 @@ async fn compile(
255198
});
256199
let status = handle.wait().await?;
257200
if !status.success() {
258-
bail!("Build failed. Is the Shuttle runtime missing?");
201+
bail!("Build failed.");
259202
}
260203

261-
let services = packages
262-
.iter()
263-
.map(|package| {
264-
let mut path: PathBuf = [
265-
project_path.clone(),
266-
target_path.clone(),
267-
profile.into(),
268-
package.name.clone().into(),
269-
]
270-
.iter()
271-
.collect();
272-
path.set_extension(std::env::consts::EXE_EXTENSION);
273-
274-
BuiltService {
275-
workspace_path: project_path.clone(),
276-
manifest_path: package.manifest_path.clone().into_std_path_buf(),
277-
package_name: package.name.clone(),
278-
executable_path: path,
279-
}
280-
})
281-
.collect();
282-
283-
Ok(services)
204+
let mut path: PathBuf = [
205+
project_path.clone(),
206+
target_path.clone(),
207+
profile.into(),
208+
target.name.as_str().into(),
209+
]
210+
.iter()
211+
.collect();
212+
path.set_extension(std::env::consts::EXE_EXTENSION);
213+
214+
Ok(BuiltService {
215+
workspace_path: project_path.clone(),
216+
target_name: target.name,
217+
executable_path: path,
218+
})
284219
}

cargo-shuttle/src/lib.rs

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ use crate::args::{
6161
ResourceCommand, SecretsArgs, TableArgs, TemplateLocation,
6262
};
6363
pub use crate::args::{Command, ProjectArgs, RunArgs, ShuttleArgs};
64-
use crate::builder::{async_cargo_metadata, build_workspace, find_shuttle_packages, BuiltService};
64+
use crate::builder::{
65+
async_cargo_metadata, build_workspace, find_first_shuttle_package, BuiltService,
66+
};
6567
use crate::config::RequestContext;
6668
use crate::provisioner_server::{ProvApiState, ProvisionerServer};
6769
use crate::util::{
@@ -1293,7 +1295,7 @@ impl Shuttle {
12931295
})
12941296
}
12951297

1296-
async fn pre_local_run(&self, run_args: &RunArgs) -> Result<Vec<BuiltService>> {
1298+
async fn pre_local_run(&self, run_args: &RunArgs) -> Result<BuiltService> {
12971299
trace!("starting a local run with args: {run_args:?}");
12981300

12991301
let (tx, mut rx) = tokio::sync::mpsc::channel::<String>(256);
@@ -1311,7 +1313,7 @@ impl Shuttle {
13111313
working_directory.display()
13121314
);
13131315

1314-
build_workspace(working_directory, run_args.release, tx, false).await
1316+
build_workspace(working_directory, run_args.release, tx).await
13151317
}
13161318

13171319
fn find_available_port(run_args: &mut RunArgs) {
@@ -1346,12 +1348,7 @@ impl Shuttle {
13461348
return bacon::run_bacon(working_directory).await;
13471349
}
13481350

1349-
let services = self.pre_local_run(&run_args).await?;
1350-
let service = services
1351-
.first()
1352-
.expect("at least one shuttle service")
1353-
.to_owned();
1354-
1351+
let service = self.pre_local_run(&run_args).await?;
13551352
trace!(path = ?service.executable_path, "runtime executable");
13561353

13571354
let secrets = Shuttle::get_secrets(&run_args.secret_args, working_directory, true)?
@@ -1382,7 +1379,7 @@ impl Shuttle {
13821379
println!(
13831380
"\n {} {} on http://{}:{}\n",
13841381
"Starting".bold().green(),
1385-
service.package_name,
1382+
service.target_name,
13861383
ip,
13871384
run_args.port,
13881385
);
@@ -1592,13 +1589,10 @@ impl Shuttle {
15921589
let mut rust_build_args = BuildArgsRust::default();
15931590

15941591
let metadata = async_cargo_metadata(manifest_path.as_path()).await?;
1595-
let packages = find_shuttle_packages(&metadata)?;
15961592
// TODO: support overriding this
1597-
let package = packages
1598-
.first()
1599-
.expect("Expected at least one crate with shuttle-runtime in the workspace");
1600-
let package_name = package.name.to_owned();
1601-
rust_build_args.package_name = Some(package_name);
1593+
let (package, target) = find_first_shuttle_package(&metadata)?;
1594+
rust_build_args.package_name = Some(package.name.clone());
1595+
rust_build_args.binary_name = Some(target.name.clone());
16021596

16031597
// activate shuttle feature if present
16041598
let (no_default_features, features) = if package.features.contains_key("shuttle") {

0 commit comments

Comments
 (0)