Skip to content

Commit 8c3c1ea

Browse files
Add build_manifest to main macro output (#2062)
* feat: Write provision manifest file * feat: Add provision manifest to build args * fix: Add error handling to build manifest export * fix: Clippy * feat: Add project status updates for instance size * fix: Clippy * docs: Better code documentation * fix: Cargo make types * fix: Specify working directory in passing build_manifest to deploy API * fix: Check CARGO_MANIFEST_DIR/.shuttle/build_manifest * fix: Remove Configured via .. macro info * fix: root relative path * nit: fix variable name --------- Co-authored-by: jonaro00 <[email protected]>
1 parent 201b61a commit 8c3c1ea

File tree

8 files changed

+216
-36
lines changed

8 files changed

+216
-36
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cargo-shuttle/src/config.rs

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,22 @@ use crate::init::create_or_update_ignore_file;
1111

1212
/// An impl of [`ConfigManager`] which is localised to a working directory
1313
pub struct LocalConfigManager {
14-
working_directory: PathBuf,
14+
directory: PathBuf,
1515
file_name: String,
1616
}
1717

1818
impl LocalConfigManager {
19-
pub fn new<P: AsRef<Path>>(working_directory: P, file_name: String) -> Self {
19+
pub fn new<P: AsRef<Path>>(directory: P, file_name: String) -> Self {
2020
Self {
21-
working_directory: working_directory.as_ref().to_path_buf(),
21+
directory: directory.as_ref().to_path_buf(),
2222
file_name,
2323
}
2424
}
2525
}
2626

2727
impl ConfigManager for LocalConfigManager {
2828
fn directory(&self) -> PathBuf {
29-
self.working_directory.clone()
29+
self.directory.clone()
3030
}
3131

3232
fn filename(&self) -> PathBuf {
@@ -159,7 +159,7 @@ impl RequestContext {
159159
.as_ref()
160160
.unwrap()
161161
.manager
162-
.working_directory
162+
.directory
163163
.join(".gitignore"),
164164
)
165165
.context("Failed to create .gitignore file")?;
@@ -260,17 +260,12 @@ impl RequestContext {
260260
}
261261
}
262262

263-
/// Get the current context working directory
263+
/// Get the cargo workspace root directory
264264
///
265265
/// # Panics
266266
/// Panics if project configuration has not been loaded.
267-
pub fn working_directory(&self) -> &Path {
268-
self.project
269-
.as_ref()
270-
.unwrap()
271-
.manager
272-
.working_directory
273-
.as_path()
267+
pub fn project_directory(&self) -> &Path {
268+
self.project.as_ref().unwrap().manager.directory.as_path()
274269
}
275270

276271
/// Set the API key to the global configuration. Will persist the file.

cargo-shuttle/src/lib.rs

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1305,15 +1305,15 @@ impl Shuttle {
13051305
}
13061306
});
13071307

1308-
let working_directory = self.ctx.working_directory();
1308+
let project_directory = self.ctx.project_directory();
13091309

13101310
println!(
13111311
"{} {}",
13121312
" Building".bold().green(),
1313-
working_directory.display()
1313+
project_directory.display()
13141314
);
13151315

1316-
build_workspace(working_directory, run_args.release, tx).await
1316+
build_workspace(project_directory, run_args.release, tx).await
13171317
}
13181318

13191319
fn find_available_port(run_args: &mut RunArgs) {
@@ -1336,7 +1336,7 @@ impl Shuttle {
13361336

13371337
async fn local_run(&self, mut run_args: RunArgs, debug: bool) -> Result<()> {
13381338
let project_name = self.ctx.project_name().to_owned();
1339-
let working_directory = self.ctx.working_directory();
1339+
let project_directory = self.ctx.project_directory();
13401340

13411341
// Handle bacon mode
13421342
if run_args.bacon {
@@ -1345,13 +1345,13 @@ impl Shuttle {
13451345
"Starting".bold().green(),
13461346
project_name
13471347
);
1348-
return bacon::run_bacon(working_directory).await;
1348+
return bacon::run_bacon(project_directory).await;
13491349
}
13501350

13511351
let service = self.pre_local_run(&run_args).await?;
13521352
trace!(path = ?service.executable_path, "runtime executable");
13531353

1354-
let secrets = Shuttle::get_secrets(&run_args.secret_args, working_directory, true)?
1354+
let secrets = Shuttle::get_secrets(&run_args.secret_args, project_directory, true)?
13551355
.unwrap_or_default();
13561356
Shuttle::find_available_port(&mut run_args);
13571357
if let Some(warning) = check_and_warn_runtime_version(&service.executable_path).await? {
@@ -1544,10 +1544,10 @@ impl Shuttle {
15441544

15451545
async fn deploy(&mut self, args: DeployArgs) -> Result<()> {
15461546
let client = self.client.as_ref().unwrap();
1547-
let working_directory = self.ctx.working_directory();
1548-
let manifest_path = working_directory.join("Cargo.toml");
1547+
let project_directory = self.ctx.project_directory();
1548+
let manifest_path = project_directory.join("Cargo.toml");
15491549

1550-
let secrets = Shuttle::get_secrets(&args.secret_args, working_directory, false)?;
1550+
let secrets = Shuttle::get_secrets(&args.secret_args, project_directory, false)?;
15511551

15521552
// Image deployment mode
15531553
if let Some(image) = args.image {
@@ -1603,6 +1603,14 @@ impl Shuttle {
16031603
rust_build_args.no_default_features = no_default_features;
16041604
rust_build_args.features = features.map(|v| v.join(","));
16051605

1606+
// Look for a build manifest file at .shuttle/build_manifest.json which specifies resources
1607+
// that need to be provisioned for the application
1608+
let default_manifest = Path::new(".shuttle").join("build_manifest.json");
1609+
if std::fs::exists(project_directory.join(&default_manifest)).is_ok() {
1610+
rust_build_args.provision_manifest =
1611+
default_manifest.into_os_string().into_string().ok();
1612+
}
1613+
16061614
rust_build_args.shuttle_runtime_version = package
16071615
.dependencies
16081616
.iter()
@@ -1620,7 +1628,7 @@ impl Shuttle {
16201628

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

1623-
if let Ok(repo) = Repository::discover(working_directory) {
1631+
if let Ok(repo) = Repository::discover(project_directory) {
16241632
let repo_path = repo
16251633
.workdir()
16261634
.context("getting working directory of repository")?;
@@ -1672,7 +1680,10 @@ impl Shuttle {
16721680

16731681
eprintln!("Creating deployment...");
16741682
let (deployment, raw_json) = client
1675-
.deploy(pid, DeploymentRequest::BuildArchive(deployment_req))
1683+
.deploy(
1684+
pid,
1685+
DeploymentRequest::BuildArchive(Box::new(deployment_req)),
1686+
)
16761687
.await?
16771688
.into_parts();
16781689

@@ -1891,7 +1902,7 @@ impl Shuttle {
18911902
fn make_archive(&self, secrets_file: Option<PathBuf>) -> Result<Vec<u8>> {
18921903
let include_patterns = self.ctx.include();
18931904

1894-
let working_directory = self.ctx.working_directory();
1905+
let project_directory = self.ctx.project_directory();
18951906

18961907
//
18971908
// Mixing include and exclude overrides messes up the .ignore and .gitignore etc,
@@ -1900,7 +1911,7 @@ impl Shuttle {
19001911
let mut entries = Vec::new();
19011912

19021913
// Default excludes
1903-
let ignore_overrides = OverrideBuilder::new(working_directory)
1914+
let ignore_overrides = OverrideBuilder::new(project_directory)
19041915
.add("!.git/")
19051916
.context("adding override `!.git/`")?
19061917
.add("!target/")
@@ -1909,7 +1920,7 @@ impl Shuttle {
19091920
.context(format!("adding override `!{STORAGE_DIRNAME}/`"))?
19101921
.build()
19111922
.context("building archive override rules")?;
1912-
for r in WalkBuilder::new(working_directory)
1923+
for r in WalkBuilder::new(project_directory)
19131924
.hidden(false)
19141925
.overrides(ignore_overrides)
19151926
.build()
@@ -1935,10 +1946,10 @@ impl Shuttle {
19351946

19361947
// Find the files
19371948
let globs = globs.build().context("glob glob")?;
1938-
for entry in walkdir::WalkDir::new(working_directory) {
1949+
for entry in walkdir::WalkDir::new(project_directory) {
19391950
let path = entry.context("list dir")?.into_path();
19401951
if globs.is_match(
1941-
path.strip_prefix(working_directory)
1952+
path.strip_prefix(project_directory)
19421953
.context("strip prefix of path")?,
19431954
) {
19441955
entries.push(path);
@@ -1960,7 +1971,7 @@ impl Shuttle {
19601971

19611972
// zip file puts all files in root
19621973
let mut name = path
1963-
.strip_prefix(working_directory)
1974+
.strip_prefix(project_directory)
19641975
.context("strip prefix of path")?
19651976
.to_owned();
19661977

codegen/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ proc-macro = true
1414
proc-macro-error2 = { workspace = true }
1515
proc-macro2 = { workspace = true }
1616
quote = { workspace = true }
17+
serde = { workspace = true }
18+
serde_json = { workspace = true }
19+
shuttle-common = { workspace = true }
1720
syn = { workspace = true, features = ["full", "extra-traits"] }
1821

1922
[dev-dependencies]
2023
pretty_assertions = { workspace = true }
2124
serde = { workspace = true }
22-
serde_json = { workspace = true }
2325
trybuild = { workspace = true }
2426
tokio = { workspace = true, features = ["full"] }

codegen/src/shuttle_main.rs

Lines changed: 155 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,167 @@ use proc_macro::TokenStream;
22
use proc_macro2::Span;
33
use proc_macro_error2::emit_error;
44
use quote::{quote, ToTokens};
5+
use serde::{ser::SerializeStruct, Serialize};
56
use syn::{
6-
parse::Parse, parse_macro_input, parse_quote, punctuated::Punctuated, spanned::Spanned,
7-
Attribute, Expr, ExprLit, FnArg, Ident, ItemFn, Lit, Pat, PatIdent, Path, ReturnType,
8-
Signature, Stmt, Token, Type, TypePath,
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,
12+
ReturnType, Signature, Stmt, Token, Type, TypePath,
913
};
1014

11-
pub(crate) fn tokens(_attr: TokenStream, item: TokenStream) -> TokenStream {
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.
96+
///
97+
/// This function processes the attribute arguments and the annotated function,
98+
/// generating code that:
99+
///
100+
/// 1. Serializes the attribute arguments to a build manifest file
101+
/// 2. Transforms the user's main function to support Shuttle's deployment model
102+
/// 3. Generates loader and runner functions to handle resource initialization
103+
///
104+
/// The generated code will:
105+
/// - Create a Tokio runtime to run the async code
106+
/// - Process any resource attributes on function parameters
107+
/// - Handle secret interpolation in resource configuration strings
108+
/// - Invoke the Shuttle framework's startup mechanism
109+
///
110+
/// # Arguments
111+
///
112+
/// * `attr` - The TokenStream representing the attribute arguments
113+
/// * `item` - The TokenStream representing the annotated function
114+
///
115+
pub(crate) fn tokens(attr: TokenStream, item: TokenStream) -> TokenStream {
12116
let mut user_main_fn = parse_macro_input!(item as ItemFn);
13117
let loader_runner = LoaderAndRunner::from_item_fn(&mut user_main_fn);
14118

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+
// --
165+
15166
Into::into(quote! {
16167
fn main() {
17168
// manual expansion of #[tokio::main]

0 commit comments

Comments
 (0)