Skip to content

Commit 01919e1

Browse files
committed
Build profiles
Signed-off-by: itowlson <[email protected]>
1 parent f51829e commit 01919e1

File tree

19 files changed

+268
-41
lines changed

19 files changed

+268
-41
lines changed

crates/build/src/lib.rs

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,13 @@ use subprocess::{Exec, Redirection};
1515

1616
use crate::manifest::component_build_configs;
1717

18+
const LAST_BUILD_PROFILE_FILE: &str = "last-build.txt";
19+
const LAST_BUILD_ANON_VALUE: &str = "<anonymous>";
20+
1821
/// If present, run the build command of each component.
1922
pub async fn build(
2023
manifest_file: &Path,
24+
profile: Option<&str>,
2125
component_ids: &[String],
2226
target_checks: TargetChecking,
2327
cache_root: Option<PathBuf>,
@@ -32,7 +36,7 @@ pub async fn build(
3236
})?;
3337
let app_dir = parent_dir(manifest_file)?;
3438

35-
let build_result = build_components(component_ids, build_info.components(), &app_dir);
39+
let build_result = build_components(component_ids, build_info.components(), &app_dir, profile);
3640

3741
// Emit any required warnings now, so that they don't bury any errors.
3842
if let Some(e) = build_info.load_error() {
@@ -53,6 +57,8 @@ pub async fn build(
5357
// If the build failed, exit with an error at this point.
5458
build_result?;
5559

60+
save_last_build_profile(&app_dir, profile);
61+
5662
let Some(manifest) = build_info.manifest() else {
5763
// We can't proceed to checking (because that needs a full healthy manifest), and we've
5864
// already emitted any necessary warning, so quit.
@@ -89,14 +95,26 @@ pub async fn build(
8995
/// Run all component build commands, using the default options (build all
9096
/// components, perform target checking). We run a "default build" in several
9197
/// places and this centralises the logic of what such a "default build" means.
92-
pub async fn build_default(manifest_file: &Path, cache_root: Option<PathBuf>) -> Result<()> {
93-
build(manifest_file, &[], TargetChecking::Check, cache_root).await
98+
pub async fn build_default(
99+
manifest_file: &Path,
100+
profile: Option<&str>,
101+
cache_root: Option<PathBuf>,
102+
) -> Result<()> {
103+
build(
104+
manifest_file,
105+
profile,
106+
&[],
107+
TargetChecking::Check,
108+
cache_root,
109+
)
110+
.await
94111
}
95112

96113
fn build_components(
97114
component_ids: &[String],
98115
components: Vec<ComponentBuildInfo>,
99116
app_dir: &Path,
117+
profile: Option<&str>,
100118
) -> Result<(), anyhow::Error> {
101119
let components_to_build = if component_ids.is_empty() {
102120
components
@@ -126,18 +144,24 @@ fn build_components(
126144

127145
components_to_build
128146
.into_iter()
129-
.map(|c| build_component(c, app_dir))
147+
.map(|c| build_component(c, app_dir, profile))
130148
.collect::<Result<Vec<_>, _>>()?;
131149

132150
terminal::step!("Finished", "building all Spin components");
133151
Ok(())
134152
}
135153

136154
/// Run the build command of the component.
137-
fn build_component(build_info: ComponentBuildInfo, app_dir: &Path) -> Result<()> {
155+
fn build_component(
156+
build_info: ComponentBuildInfo,
157+
app_dir: &Path,
158+
profile: Option<&str>,
159+
) -> Result<()> {
138160
match build_info.build {
139161
Some(b) => {
140-
let command_count = b.commands().len();
162+
let commands = b.commands(profile);
163+
164+
let command_count = commands.len();
141165

142166
if command_count > 1 {
143167
terminal::step!(
@@ -148,7 +172,7 @@ fn build_component(build_info: ComponentBuildInfo, app_dir: &Path) -> Result<()>
148172
);
149173
}
150174

151-
for (index, command) in b.commands().enumerate() {
175+
for (index, command) in commands.into_iter().enumerate() {
152176
if command_count > 1 {
153177
terminal::step!(
154178
"Running build step",
@@ -215,6 +239,56 @@ fn construct_workdir(app_dir: &Path, workdir: Option<impl AsRef<Path>>) -> Resul
215239
Ok(cwd)
216240
}
217241

242+
/// Saves the build profile to the "last build profile" file.
243+
/// Errors are ignored as they should not block building.
244+
pub fn save_last_build_profile(app_dir: &Path, profile: Option<&str>) {
245+
let app_stash_dir = app_dir.join(".spin");
246+
_ = std::fs::create_dir_all(&app_stash_dir);
247+
let last_build_profile_file = app_stash_dir.join(LAST_BUILD_PROFILE_FILE);
248+
_ = std::fs::write(
249+
&last_build_profile_file,
250+
profile.unwrap_or(LAST_BUILD_ANON_VALUE),
251+
);
252+
}
253+
254+
/// Reads the last build profile from the "last build profile" file.
255+
/// Errors are ignored.
256+
pub fn read_last_build_profile(app_dir: &Path) -> Option<String> {
257+
let app_stash_dir = app_dir.join(".spin");
258+
let last_build_profile_file = app_stash_dir.join(LAST_BUILD_PROFILE_FILE);
259+
let last_build_str = std::fs::read_to_string(&last_build_profile_file).ok()?;
260+
261+
if last_build_str == LAST_BUILD_ANON_VALUE {
262+
None
263+
} else {
264+
Some(last_build_str)
265+
}
266+
}
267+
268+
/// Prints a warning to stderr if the given profile is not the same
269+
/// as the most recent build in the given application directory.
270+
pub fn warn_if_not_latest_build(manifest_path: &Path, profile: Option<&str>) {
271+
let Some(app_dir) = manifest_path.parent() else {
272+
return;
273+
};
274+
275+
let latest_build = read_last_build_profile(app_dir);
276+
277+
let is_match = match (profile, latest_build) {
278+
(None, None) => true,
279+
(Some(_), None) | (None, Some(_)) => false,
280+
(Some(p), Some(latest)) => p == latest,
281+
};
282+
283+
if !is_match {
284+
let profile_opt = match profile {
285+
Some(p) => format!(" --profile {p}"),
286+
None => "".to_string(),
287+
};
288+
terminal::warn!("You built a different profile more recently than the one you are running. If the app appears to be behaving like an older version then run `spin up --build{profile_opt}`.");
289+
}
290+
}
291+
218292
/// Specifies target environment checking behaviour
219293
pub enum TargetChecking {
220294
/// The build should check that all components are compatible with all target environments.
@@ -242,23 +316,23 @@ mod tests {
242316
#[tokio::test]
243317
async fn can_load_even_if_trigger_invalid() {
244318
let bad_trigger_file = test_data_root().join("bad_trigger.toml");
245-
build(&bad_trigger_file, &[], TargetChecking::Skip, None)
319+
build(&bad_trigger_file, None, &[], TargetChecking::Skip, None)
246320
.await
247321
.unwrap();
248322
}
249323

250324
#[tokio::test]
251325
async fn succeeds_if_target_env_matches() {
252326
let manifest_path = test_data_root().join("good_target_env.toml");
253-
build(&manifest_path, &[], TargetChecking::Check, None)
327+
build(&manifest_path, None, &[], TargetChecking::Check, None)
254328
.await
255329
.unwrap();
256330
}
257331

258332
#[tokio::test]
259333
async fn fails_if_target_env_does_not_match() {
260334
let manifest_path = test_data_root().join("bad_target_env.toml");
261-
let err = build(&manifest_path, &[], TargetChecking::Check, None)
335+
let err = build(&manifest_path, None, &[], TargetChecking::Check, None)
262336
.await
263337
.expect_err("should have failed")
264338
.to_string();

crates/doctor/src/rustlang/target.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ impl Diagnostic for TargetDiagnostic {
1919
let uses_rust = manifest.components.values().any(|c| {
2020
c.build
2121
.as_ref()
22-
.map(|b| b.commands().any(|c| c.starts_with("cargo")))
22+
.map(|b| b.commands(None).iter().any(|c| c.starts_with("cargo")))
2323
.unwrap_or_default()
2424
});
2525

crates/factors-test/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,5 +102,5 @@ pub async fn build_locked_app(manifest: &toml::Table) -> anyhow::Result<LockedAp
102102
let dir = tempfile::tempdir().context("failed creating tempdir")?;
103103
let path = dir.path().join("spin.toml");
104104
std::fs::write(&path, toml_str).context("failed writing manifest")?;
105-
spin_loader::from_file(&path, FilesMountStrategy::Direct, None).await
105+
spin_loader::from_file(&path, FilesMountStrategy::Direct, None, None).await
106106
}

crates/loader/src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,20 @@ pub(crate) const MAX_FILE_LOADING_CONCURRENCY: usize = 16;
3535
pub async fn from_file(
3636
manifest_path: impl AsRef<Path>,
3737
files_mount_strategy: FilesMountStrategy,
38+
profile: Option<&str>,
3839
cache_root: Option<PathBuf>,
3940
) -> Result<LockedApp> {
4041
let path = manifest_path.as_ref();
4142
let app_root = parent_dir(path).context("manifest path has no parent directory")?;
42-
let loader = LocalLoader::new(&app_root, files_mount_strategy, cache_root).await?;
43+
let loader = LocalLoader::new(&app_root, files_mount_strategy, profile, cache_root).await?;
4344
loader.load_file(path).await
4445
}
4546

4647
/// Load a Spin locked app from a standalone Wasm file.
4748
pub async fn from_wasm_file(wasm_path: impl AsRef<Path>) -> Result<LockedApp> {
4849
let app_root = std::env::current_dir()?;
4950
let manifest = single_file_manifest(wasm_path)?;
50-
let loader = LocalLoader::new(&app_root, FilesMountStrategy::Direct, None).await?;
51+
let loader = LocalLoader::new(&app_root, FilesMountStrategy::Direct, None, None).await?;
5152
loader.load_manifest(manifest).await
5253
}
5354

crates/loader/src/local.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,14 @@ pub struct LocalLoader {
2626
files_mount_strategy: FilesMountStrategy,
2727
file_loading_permits: std::sync::Arc<Semaphore>,
2828
wasm_loader: WasmLoader,
29+
profile: Option<String>,
2930
}
3031

3132
impl LocalLoader {
3233
pub async fn new(
3334
app_root: &Path,
3435
files_mount_strategy: FilesMountStrategy,
36+
profile: Option<&str>,
3537
cache_root: Option<PathBuf>,
3638
) -> Result<Self> {
3739
let app_root = safe_canonicalize(app_root)
@@ -44,6 +46,7 @@ impl LocalLoader {
4446
// Limit concurrency to avoid hitting system resource limits
4547
file_loading_permits: file_loading_permits.clone(),
4648
wasm_loader: WasmLoader::new(app_root, cache_root, Some(file_loading_permits)).await?,
49+
profile: profile.map(|s| s.to_owned()),
4750
})
4851
}
4952

@@ -68,6 +71,13 @@ impl LocalLoader {
6871
.metadata
6972
.insert("origin".into(), file_url(path)?.into());
7073

74+
// Set build profile metadata
75+
if let Some(profile) = self.profile.as_ref() {
76+
locked
77+
.metadata
78+
.insert("profile".into(), profile.as_str().into());
79+
}
80+
7181
Ok(locked)
7282
}
7383

@@ -155,6 +165,11 @@ impl LocalLoader {
155165

156166
let component_requires_service_chaining = requires_service_chaining(&component);
157167

168+
let source = self
169+
.load_component_source(id, component.source(self.profile.as_ref()))
170+
.await
171+
.with_context(|| format!("Failed to load Wasm source {}", component.source))?;
172+
158173
let metadata = ValuesMapBuilder::new()
159174
.string("description", component.description)
160175
.string_array("allowed_outbound_hosts", allowed_outbound_hosts)
@@ -164,11 +179,6 @@ impl LocalLoader {
164179
.serializable("build", component.build)?
165180
.take();
166181

167-
let source = self
168-
.load_component_source(id, component.source.clone())
169-
.await
170-
.with_context(|| format!("Failed to load Wasm source {}", component.source))?;
171-
172182
let dependencies = self
173183
.load_component_dependencies(
174184
id,
@@ -925,6 +935,7 @@ mod test {
925935
&app_root,
926936
FilesMountStrategy::Copy(wd.path().to_owned()),
927937
None,
938+
None,
928939
)
929940
.await?;
930941
let err = loader

crates/loader/tests/ui.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ fn run_test(input: &Path, normalizer: &mut Normalizer) -> Result<String, Failed>
5050
input,
5151
spin_loader::FilesMountStrategy::Copy(files_mount_root),
5252
None,
53+
None,
5354
)
5455
.await
5556
.map_err(|err| format!("{err:?}"))?;

crates/manifest/src/compat.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ pub fn v1_to_v2_app(manifest: v1::AppManifestV1) -> Result<v2::AppManifest, Erro
7878
allowed_http_hosts: Vec::new(),
7979
dependencies_inherit_configuration: false,
8080
dependencies: Default::default(),
81+
profile: Default::default(),
8182
},
8283
);
8384
triggers

crates/manifest/src/schema/common.rs

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -161,13 +161,18 @@ pub enum WasiFilesMount {
161161
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
162162
#[serde(deny_unknown_fields)]
163163
pub struct ComponentBuildConfig {
164-
/// The command or commands to build the application. If multiple commands
164+
/// The command or commands to build the component. If multiple commands
165165
/// are specified, they are run sequentially from left to right.
166166
///
167-
/// Example: `command = "cargo build"`, `command = ["npm install", "npm run build"]`
167+
/// Example: `command = "cargo build --release"`, `command = ["npm install", "npm run build"]`
168168
///
169169
/// Learn more: https://spinframework.dev/build#setting-up-for-spin-build
170170
pub command: Commands,
171+
/// The command or commands for building the component in non-default profiles.
172+
/// If a component has no special build instructions for a profile, the
173+
/// default build command is used.
174+
#[serde(default, skip_serializing_if = "super::v2::Map::is_empty")]
175+
pub profile: super::v2::Map<String, ComponentProfileBuildCommand>,
171176
/// The working directory for the build command. If omitted, the build working
172177
/// directory is the directory containing `spin.toml`.
173178
///
@@ -188,14 +193,28 @@ pub struct ComponentBuildConfig {
188193
pub watch: Vec<String>,
189194
}
190195

196+
/// Component build configuration
197+
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
198+
#[serde(deny_unknown_fields)]
199+
pub struct ComponentProfileBuildCommand {
200+
/// The command or commands to build the component in a named profile. If multiple commands
201+
/// are specified, they are run sequentially from left to right.
202+
///
203+
/// Example: `profile.debug.command = "cargo build"`
204+
///
205+
/// Learn more: https://spinframework.dev/build#setting-up-for-spin-build
206+
pub command: Commands,
207+
}
208+
191209
impl ComponentBuildConfig {
192210
/// The commands to execute for the build
193-
pub fn commands(&self) -> impl ExactSizeIterator<Item = &String> {
194-
let as_vec = match &self.command {
195-
Commands::Single(cmd) => vec![cmd],
196-
Commands::Multiple(cmds) => cmds.iter().collect(),
197-
};
198-
as_vec.into_iter()
211+
pub fn commands(&self, profile: Option<&str>) -> Vec<&String> {
212+
if let Some(profile) = profile {
213+
if let Some(profile_cmd) = self.profile.get(profile) {
214+
return profile_cmd.command.as_vec();
215+
}
216+
}
217+
self.command.as_vec()
199218
}
200219
}
201220

@@ -216,6 +235,15 @@ pub enum Commands {
216235
Multiple(Vec<String>),
217236
}
218237

238+
impl Commands {
239+
fn as_vec(&self) -> Vec<&String> {
240+
match self {
241+
Self::Single(cmd) => vec![cmd],
242+
Self::Multiple(cmds) => cmds.iter().collect(),
243+
}
244+
}
245+
}
246+
219247
fn is_false(v: &bool) -> bool {
220248
!*v
221249
}

0 commit comments

Comments
 (0)