Skip to content

Commit b0d3eda

Browse files
committed
Build profiles
Signed-off-by: itowlson <[email protected]>
1 parent 40a188b commit b0d3eda

File tree

20 files changed

+484
-33
lines changed

20 files changed

+484
-33
lines changed

crates/build/src/lib.rs

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,18 @@ 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>,
2428
) -> Result<()> {
25-
let build_info = component_build_configs(manifest_file)
29+
let build_info = component_build_configs(manifest_file, profile)
2630
.await
2731
.with_context(|| {
2832
format!(
@@ -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,8 +95,19 @@ 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(
@@ -215,6 +232,50 @@ fn construct_workdir(app_dir: &Path, workdir: Option<impl AsRef<Path>>) -> Resul
215232
Ok(cwd)
216233
}
217234

235+
/// Saves the build profile to the "last build profile" file.
236+
/// Errors are ignored as they should not block building.
237+
pub fn save_last_build_profile(app_dir: &Path, profile: Option<&str>) {
238+
let app_stash_dir = app_dir.join(".spin");
239+
_ = std::fs::create_dir_all(&app_stash_dir);
240+
let last_build_profile_file = app_stash_dir.join(LAST_BUILD_PROFILE_FILE);
241+
_ = std::fs::write(
242+
&last_build_profile_file,
243+
profile.unwrap_or(LAST_BUILD_ANON_VALUE),
244+
);
245+
}
246+
247+
/// Reads the last build profile from the "last build profile" file.
248+
/// Errors are ignored.
249+
pub fn read_last_build_profile(app_dir: &Path) -> Option<String> {
250+
let app_stash_dir = app_dir.join(".spin");
251+
let last_build_profile_file = app_stash_dir.join(LAST_BUILD_PROFILE_FILE);
252+
let last_build_str = std::fs::read_to_string(&last_build_profile_file).ok()?;
253+
254+
if last_build_str == LAST_BUILD_ANON_VALUE {
255+
None
256+
} else {
257+
Some(last_build_str)
258+
}
259+
}
260+
261+
/// Prints a warning to stderr if the given profile is not the same
262+
/// as the most recent build in the given application directory.
263+
pub fn warn_if_not_latest_build(manifest_path: &Path, profile: Option<&str>) {
264+
let Some(app_dir) = manifest_path.parent() else {
265+
return;
266+
};
267+
268+
let latest_build = read_last_build_profile(app_dir);
269+
270+
if profile != latest_build.as_deref() {
271+
let profile_opt = match profile {
272+
Some(p) => format!(" --profile {p}"),
273+
None => "".to_string(),
274+
};
275+
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}`.");
276+
}
277+
}
278+
218279
/// Specifies target environment checking behaviour
219280
pub enum TargetChecking {
220281
/// The build should check that all components are compatible with all target environments.
@@ -242,23 +303,23 @@ mod tests {
242303
#[tokio::test]
243304
async fn can_load_even_if_trigger_invalid() {
244305
let bad_trigger_file = test_data_root().join("bad_trigger.toml");
245-
build(&bad_trigger_file, &[], TargetChecking::Skip, None)
306+
build(&bad_trigger_file, None, &[], TargetChecking::Skip, None)
246307
.await
247308
.unwrap();
248309
}
249310

250311
#[tokio::test]
251312
async fn succeeds_if_target_env_matches() {
252313
let manifest_path = test_data_root().join("good_target_env.toml");
253-
build(&manifest_path, &[], TargetChecking::Check, None)
314+
build(&manifest_path, None, &[], TargetChecking::Check, None)
254315
.await
255316
.unwrap();
256317
}
257318

258319
#[tokio::test]
259320
async fn fails_if_target_env_does_not_match() {
260321
let manifest_path = test_data_root().join("bad_target_env.toml");
261-
let err = build(&manifest_path, &[], TargetChecking::Check, None)
322+
let err = build(&manifest_path, None, &[], TargetChecking::Check, None)
262323
.await
263324
.expect_err("should have failed")
264325
.to_string();
@@ -273,7 +334,8 @@ mod tests {
273334
#[tokio::test]
274335
async fn has_meaningful_error_if_target_env_does_not_match() {
275336
let manifest_file = test_data_root().join("bad_target_env.toml");
276-
let manifest = spin_manifest::manifest_from_file(&manifest_file).unwrap();
337+
let mut manifest = spin_manifest::manifest_from_file(&manifest_file).unwrap();
338+
spin_manifest::normalize::normalize_manifest(&mut manifest, None);
277339
let application = spin_environments::ApplicationToValidate::new(
278340
manifest.clone(),
279341
manifest_file.parent().unwrap(),

crates/build/src/manifest.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,16 @@ impl ManifestBuildInfo {
6666
/// given (v1 or v2) manifest path. If the manifest cannot be loaded, the
6767
/// function attempts fallback: if fallback succeeds, result is Ok but the load error
6868
/// is also returned via the second part of the return value tuple.
69-
pub async fn component_build_configs(manifest_file: impl AsRef<Path>) -> Result<ManifestBuildInfo> {
69+
pub async fn component_build_configs(
70+
manifest_file: impl AsRef<Path>,
71+
profile: Option<&str>,
72+
) -> Result<ManifestBuildInfo> {
7073
let manifest = spin_manifest::manifest_from_file(&manifest_file);
7174
match manifest {
7275
Ok(mut manifest) => {
73-
spin_manifest::normalize::normalize_manifest(&mut manifest);
76+
manifest.ensure_profile(profile)?;
77+
78+
spin_manifest::normalize::normalize_manifest(&mut manifest, profile);
7479
let components = build_configs_from_manifest(&manifest);
7580
let deployment_targets = deployment_targets_from_manifest(&manifest);
7681
Ok(ManifestBuildInfo::Loadable {

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: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,20 +35,21 @@ 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-
loader.load_manifest(manifest).await
51+
let loader = LocalLoader::new(&app_root, FilesMountStrategy::Direct, None, None).await?;
52+
loader.load_manifest(manifest, None).await
5253
}
5354

5455
/// The strategy to use for mounting WASI files into a guest.

crates/loader/src/local.rs

Lines changed: 22 additions & 3 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

@@ -59,7 +62,7 @@ impl LocalLoader {
5962
)
6063
})?;
6164
let mut locked = self
62-
.load_manifest(manifest)
65+
.load_manifest(manifest, self.profile())
6366
.await
6467
.with_context(|| format!("Failed to load Spin app from {}", quoted_path(path)))?;
6568

@@ -68,12 +71,23 @@ 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

7484
// Load the given manifest into a LockedApp, ready for execution.
75-
pub(crate) async fn load_manifest(&self, mut manifest: AppManifest) -> Result<LockedApp> {
76-
spin_manifest::normalize::normalize_manifest(&mut manifest);
85+
pub(crate) async fn load_manifest(
86+
&self,
87+
mut manifest: AppManifest,
88+
profile: Option<&str>,
89+
) -> Result<LockedApp> {
90+
spin_manifest::normalize::normalize_manifest(&mut manifest, profile);
7791

7892
manifest.validate_dependencies()?;
7993

@@ -538,6 +552,10 @@ impl LocalLoader {
538552
path: dest.into(),
539553
})
540554
}
555+
556+
fn profile(&self) -> Option<&str> {
557+
self.profile.as_deref()
558+
}
541559
}
542560

543561
fn explain_file_mount_source_error(e: anyhow::Error, src: &Path) -> anyhow::Error {
@@ -925,6 +943,7 @@ mod test {
925943
&app_root,
926944
FilesMountStrategy::Copy(wd.path().to_owned()),
927945
None,
946+
None,
928947
)
929948
.await?;
930949
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
@@ -73,6 +73,7 @@ pub fn v1_to_v2_app(manifest: v1::AppManifestV1) -> Result<v2::AppManifest, Erro
7373
allowed_http_hosts: Vec::new(),
7474
dependencies_inherit_configuration: false,
7575
dependencies: Default::default(),
76+
profile: Default::default(),
7677
},
7778
);
7879
triggers

crates/manifest/src/normalize.rs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ use crate::schema::v2::{AppManifest, ComponentSpec, KebabId};
88
/// - Inline components in trigger configs are moved into top-level
99
/// components and replaced with a reference.
1010
/// - Any triggers without an ID are assigned a generated ID.
11-
pub fn normalize_manifest(manifest: &mut AppManifest) {
11+
pub fn normalize_manifest(manifest: &mut AppManifest, profile: Option<&str>) {
1212
normalize_trigger_ids(manifest);
1313
normalize_inline_components(manifest);
14+
apply_profile_overrides(manifest, profile);
1415
}
1516

1617
fn normalize_inline_components(manifest: &mut AppManifest) {
@@ -103,3 +104,45 @@ fn normalize_trigger_ids(manifest: &mut AppManifest) {
103104
}
104105
}
105106
}
107+
108+
fn apply_profile_overrides(manifest: &mut AppManifest, profile: Option<&str>) {
109+
let Some(profile) = profile else {
110+
return;
111+
};
112+
113+
for (_, component) in &mut manifest.components {
114+
let Some(overrides) = component.profile.get(profile) else {
115+
continue;
116+
};
117+
118+
if let Some(profile_build) = overrides.build.as_ref() {
119+
match component.build.as_mut() {
120+
None => {
121+
component.build = Some(crate::schema::v2::ComponentBuildConfig {
122+
command: profile_build.command.clone(),
123+
workdir: None,
124+
watch: vec![],
125+
})
126+
}
127+
Some(build) => {
128+
build.command = profile_build.command.clone();
129+
}
130+
}
131+
}
132+
133+
if let Some(source) = overrides.source.as_ref() {
134+
component.source = source.clone();
135+
}
136+
137+
for (name, value) in &overrides.environment {
138+
component.environment.insert(name.clone(), value.clone());
139+
}
140+
141+
for (reference, value) in &overrides.dependencies.inner {
142+
component
143+
.dependencies
144+
.inner
145+
.insert(reference.clone(), value.clone());
146+
}
147+
}
148+
}

crates/manifest/src/schema/common.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,10 @@ 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,

0 commit comments

Comments
 (0)