Skip to content

Commit 03fa327

Browse files
authored
feat(ifc, cargo-shuttle): extract infra macro annotation parser to separate crate (#2072)
* wip: macro parsing in CLI * nits, crate name * clippy * fix: lock * feat: new infra request field * chore: switch to #[shuttle_runtime::main] meta attributes * fix: better parser, parse in main macro * replicas field (unused in parser) * fix * types * ci: infra crate * add tests for {} and [] in attribute * nit: rename to shuttle-ifc
1 parent de40cb7 commit 03fa327

File tree

17 files changed

+379
-185
lines changed

17 files changed

+379
-185
lines changed

.circleci/config.yml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ workflows:
403403
crate:
404404
- shuttle-codegen
405405
- shuttle-common
406+
- shuttle-ifc
406407
- test-workspace-member-with-integration:
407408
name: << matrix.crate >>
408409
matrix:
@@ -495,7 +496,6 @@ workflows:
495496
matrix:
496497
parameters:
497498
path:
498-
- codegen
499499
- common
500500
name: publish-<< matrix.path >>
501501
requires:
@@ -510,6 +510,7 @@ workflows:
510510
parameters:
511511
path:
512512
- api-client
513+
- ifc
513514
- service
514515
name: publish-<< matrix.path >>
515516
requires:
@@ -519,6 +520,19 @@ workflows:
519520
ignore: /.*/
520521
tags:
521522
only: /v[\d\.]+/
523+
- publish-crate:
524+
matrix:
525+
parameters:
526+
path:
527+
- codegen
528+
name: publish-<< matrix.path >>
529+
requires:
530+
- publish-ifc
531+
filters:
532+
branches:
533+
ignore: /.*/
534+
tags:
535+
only: /v[\d\.]+/
522536
- publish-crate:
523537
matrix:
524538
parameters:
@@ -546,6 +560,7 @@ workflows:
546560
name: publish-<< matrix.path >>
547561
requires:
548562
- publish-api-client
563+
- publish-codegen
549564
- publish-service
550565
filters:
551566
branches:

Cargo.lock

Lines changed: 11 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ members = [
66
"cargo-shuttle",
77
"codegen",
88
"common",
9+
"ifc",
910
"runtime",
1011
"service",
1112
]
@@ -21,6 +22,7 @@ repository = "https://github.com/shuttle-hq/shuttle"
2122
shuttle-api-client = { path = "api-client", version = "0.55.0", default-features = false }
2223
shuttle-codegen = { path = "codegen", version = "0.55.0" }
2324
shuttle-common = { path = "common", version = "0.55.0" }
25+
shuttle-ifc = { path = "ifc", version = "0.55.0" }
2426
shuttle-service = { path = "service", version = "0.55.0" }
2527

2628
anyhow = "1.0.66"

cargo-shuttle/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ default-run = "shuttle"
1111
[dependencies]
1212
shuttle-api-client = { workspace = true, default-features = true }
1313
shuttle-common = { workspace = true, features = ["models", "tables", "config"] }
14+
shuttle-ifc = { workspace = true }
1415

1516
anyhow = { workspace = true }
1617
async-trait = { workspace = true }

cargo-shuttle/src/builder.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::process::Stdio;
44

55
use anyhow::{bail, Context, Result};
66
use cargo_metadata::{Metadata, Package, Target};
7+
use shuttle_ifc::find_runtime_main_fn;
78
use tokio::io::AsyncBufReadExt;
89
use tracing::{error, trace};
910

@@ -121,10 +122,12 @@ fn find_shuttle_packages(metadata: &Metadata) -> Result<Vec<(Package, Target, Op
121122
let mut target = None;
122123
for t in member.targets.iter() {
123124
if t.is_bin()
124-
&& fs::read_to_string(t.src_path.as_std_path())
125-
.context("reading to check for shuttle macro")?
126-
// don't look for the last ] so that we also find instances of `#[shuttle_runtime::main(...)]`
127-
.contains("#[shuttle_runtime::main")
125+
&& find_runtime_main_fn(
126+
&fs::read_to_string(t.src_path.as_std_path())
127+
.context("reading to check for shuttle macro")?,
128+
)
129+
.context("parsing rust file when checking for shuttle macro")?
130+
.is_some()
128131
{
129132
target = Some(t);
130133
break;

cargo-shuttle/src/lib.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ use shuttle_common::{
4747
},
4848
tables::{deployments_table, get_certificates_table, get_projects_table, get_resource_tables},
4949
};
50+
use shuttle_ifc::parse_infra_from_code;
5051
use strum::{EnumMessage, VariantArray};
5152
use tokio::io::{AsyncBufReadExt, BufReader};
5253
use tokio::time::{sleep, Duration};
@@ -1646,20 +1647,18 @@ impl Shuttle {
16461647
rust_build_args.no_default_features = no_default_features;
16471648
rust_build_args.features = features.map(|v| v.join(","));
16481649

1649-
// Look for a build manifest file at .shuttle/build_manifest.json which specifies resources
1650-
// that need to be provisioned for the application
1651-
let default_manifest = Path::new(".shuttle").join("build_manifest.json");
1652-
if std::fs::exists(project_directory.join(&default_manifest)).is_ok() {
1653-
rust_build_args.provision_manifest =
1654-
default_manifest.into_os_string().into_string().ok();
1655-
}
1656-
16571650
// TODO: determine which (one) binary to build
16581651

16591652
deployment_req.build_args = Some(BuildArgs::Rust(rust_build_args));
16601653

16611654
// TODO: have all of the above be configurable in CLI and Shuttle.toml
16621655

1656+
deployment_req.infra = parse_infra_from_code(
1657+
&fs::read_to_string(target.src_path.as_path())
1658+
.context("reading target file when extracting infra annotations")?,
1659+
)
1660+
.context("parsing infra annotations")?;
1661+
16631662
if let Ok(repo) = Repository::discover(project_directory) {
16641663
let repo_path = repo
16651664
.workdir()

codegen/Cargo.toml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,13 @@ homepage = "https://www.shuttle.dev"
1111
proc-macro = true
1212

1313
[dependencies]
14+
shuttle-ifc = { workspace = true }
15+
1416
proc-macro-error2 = { workspace = true }
1517
proc-macro2 = { workspace = true }
1618
quote = { workspace = true }
17-
serde = { workspace = true }
18-
serde_json = { workspace = true }
19-
shuttle-common = { workspace = true }
2019
syn = { workspace = true, features = ["full", "extra-traits"] }
2120

2221
[dev-dependencies]
2322
pretty_assertions = { workspace = true }
24-
serde = { workspace = true }
2523
trybuild = { workspace = true }
26-
tokio = { workspace = true, features = ["full"] }

codegen/src/shuttle_main.rs

Lines changed: 10 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,14 @@
11
use proc_macro::TokenStream;
2-
use proc_macro2::Span;
32
use proc_macro_error2::emit_error;
43
use quote::{quote, ToTokens};
5-
use serde::{ser::SerializeStruct, Serialize};
4+
use shuttle_ifc::InfraAttrParser;
65
use syn::{
7-
parse::{Parse, ParseStream},
8-
parse_macro_input, parse_quote,
9-
punctuated::Punctuated,
10-
spanned::Spanned,
11-
Attribute, Error, Expr, ExprLit, FnArg, Ident, ItemFn, Lit, LitStr, Pat, PatIdent, Path,
6+
meta::parser, parse::Parse, parse_macro_input, parse_quote, punctuated::Punctuated,
7+
spanned::Spanned, Attribute, Expr, ExprLit, FnArg, Ident, ItemFn, Lit, Pat, PatIdent, Path,
128
ReturnType, Signature, Stmt, Token, Type, TypePath,
139
};
1410

15-
const BUILD_MANIFEST_FILE: &str = ".shuttle/build_manifest.json";
16-
17-
/// Configuration options for the `shuttle_runtime` macro.
18-
///
19-
/// This struct represents the arguments that can be passed to the
20-
/// `#[shuttle_runtime(...)]` attribute macro. It currently supports:
21-
///
22-
/// - `instance_size`: An optional string literal that specifies the size of the
23-
/// instance to use for deploying the application. For example, "xs" or "m".
24-
///
25-
/// Example usage:
26-
/// ```rust
27-
/// #[shuttle_runtime(instance_size = "l")]
28-
/// async fn main() -> ShuttleActix {
29-
/// // ...
30-
/// }
31-
/// ```
32-
#[derive(Clone, Debug, Default)]
33-
struct RuntimeMacroArgs {
34-
instance_size: Option<LitStr>,
35-
}
36-
37-
impl Serialize for RuntimeMacroArgs {
38-
/// Serializes the RuntimeMacroArgs struct into JSON.
39-
///
40-
/// This is used to write configuration to the build manifest file.
41-
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
42-
where
43-
S: serde::Serializer,
44-
{
45-
let len = self.instance_size.is_some() as usize;
46-
let mut state = serializer.serialize_struct("RuntimeMacroArgs", len)?;
47-
if let Some(instance_size) = &self.instance_size {
48-
state.serialize_field("instance_size", &instance_size.value())?;
49-
}
50-
51-
state.end()
52-
}
53-
}
54-
55-
impl Parse for RuntimeMacroArgs {
56-
/// Parses the arguments provided to the `#[shuttle_runtime(...)]` attribute macro.
57-
///
58-
/// This implementation accepts key-value pairs separated by commas, where:
59-
/// - `instance_size`: The size of the instance to use for the application
60-
///
61-
/// Returns a Result containing either the parsed RuntimeMacroArgs or a syntax error.
62-
/// If an unrecognized key is provided, it will return an error with a helpful
63-
/// message explaining the invalid key.
64-
fn parse(input: ParseStream) -> syn::Result<Self> {
65-
let mut instance_size = None;
66-
67-
while !input.is_empty() {
68-
let key: Ident = input.parse()?;
69-
input.parse::<Token![=]>()?;
70-
71-
match key.to_string().as_str() {
72-
"instance_size" => {
73-
instance_size = Some(input.parse::<LitStr>()?);
74-
}
75-
unknown_key => {
76-
return Err(syn::Error::new(
77-
key.span(),
78-
format!(
79-
"Invalid `shuttle_runtime` macro attribute key: '{}'",
80-
unknown_key
81-
),
82-
))
83-
}
84-
}
85-
86-
if !input.is_empty() {
87-
input.parse::<Token![,]>()?;
88-
}
89-
}
90-
91-
Ok(RuntimeMacroArgs { instance_size })
92-
}
93-
}
94-
95-
/// Entry point for the `#[shuttle_runtime]` attribute macro.
11+
/// Entrypoint for the `#[shuttle_runtime::main]` attribute macro.
9612
///
9713
/// This function processes the attribute arguments and the annotated function,
9814
/// generating code that:
@@ -116,52 +32,10 @@ pub(crate) fn tokens(attr: TokenStream, item: TokenStream) -> TokenStream {
11632
let mut user_main_fn = parse_macro_input!(item as ItemFn);
11733
let loader_runner = LoaderAndRunner::from_item_fn(&mut user_main_fn);
11834

119-
// Start write build manifest - to be replaced by a syn parse stage
120-
// --
121-
let attr_ast = parse_macro_input!(attr as RuntimeMacroArgs);
122-
123-
let json_str = match serde_json::to_string(&attr_ast).map_err(|err| {
124-
Error::new(
125-
user_main_fn.span(),
126-
format!("failed to serialize build manifest: {:?}", err),
127-
)
128-
}) {
129-
Ok(json) => json,
130-
Err(e) => return e.into_compile_error().into(),
131-
};
132-
133-
if let Some(shuttle_dir) = std::path::Path::new(BUILD_MANIFEST_FILE).parent() {
134-
if !shuttle_dir.exists() {
135-
match std::fs::create_dir_all(shuttle_dir).map_err(|err| {
136-
Error::new(
137-
user_main_fn.span(),
138-
format!(
139-
"failed to create shuttle directory: {:?}: {}",
140-
shuttle_dir, err
141-
),
142-
)
143-
}) {
144-
Ok(_) => (),
145-
Err(e) => return e.into_compile_error().into(),
146-
};
147-
}
148-
}
149-
150-
match std::fs::write(BUILD_MANIFEST_FILE, json_str).map_err(|err| {
151-
Error::new(
152-
user_main_fn.span(),
153-
format!(
154-
"failed to write build manifest to '{}': {:?}",
155-
BUILD_MANIFEST_FILE, err
156-
),
157-
)
158-
}) {
159-
Ok(_) => (),
160-
Err(e) => return e.into_compile_error().into(),
161-
};
162-
163-
// End write build manifest
164-
// --
35+
// parse infra in main attribute
36+
let mut infra_parser = InfraAttrParser::default();
37+
let meta_parser = parser(|meta| infra_parser.parse_nested_meta(meta));
38+
parse_macro_input!(attr with meta_parser);
16539

16640
Into::into(quote! {
16741
fn main() {
@@ -252,9 +126,10 @@ impl LoaderAndRunner {
252126
// prefix the function name to allow any name, such as 'main'
253127
item_fn.sig.ident = Ident::new(
254128
&format!("__shuttle_{}", item_fn.sig.ident),
255-
Span::call_site(),
129+
item_fn.sig.ident.span(),
256130
);
257131

132+
// parse builders from function arguments
258133
let inputs: Vec<_> = item_fn
259134
.sig
260135
.inputs
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#[shuttle_codegen::main(something = "not right")]
2+
async fn bad_infra() -> ShuttleRocket {}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
error: Invalid macro attribute key: 'something'
2+
--> tests/compiler_output/bad-infra.rs:1:1
3+
|
4+
1 | #[shuttle_codegen::main(something = "not right")]
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
6+
|
7+
= note: this error originates in the attribute macro `shuttle_codegen::main` (in Nightly builds, run with -Z macro-backtrace for more info)
8+
9+
error[E0601]: `main` function not found in crate `$CRATE`
10+
--> tests/compiler_output/bad-infra.rs:2:41
11+
|
12+
2 | async fn bad_infra() -> ShuttleRocket {}
13+
| ^ consider adding a `main` function to `$DIR/tests/compiler_output/bad-infra.rs`

0 commit comments

Comments
 (0)