Skip to content

Commit 9e78516

Browse files
committed
Build profiles
Signed-off-by: itowlson <[email protected]>
1 parent 22322aa commit 9e78516

File tree

22 files changed

+555
-33
lines changed

22 files changed

+555
-33
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.

crates/build/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ subprocess = "0.2"
1414
terminal = { path = "../terminal" }
1515
tokio = { workspace = true, features = ["full"] }
1616
toml = { workspace = true }
17+
tracing = { workspace = true }

crates/build/src/lib.rs

Lines changed: 90 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,10 @@ pub async fn build(
5357
// If the build failed, exit with an error at this point.
5458
build_result?;
5559

60+
if let Err(e) = save_last_build_profile(&app_dir, profile) {
61+
tracing::warn!("Failed to save build profile: {e:?}");
62+
}
63+
5664
let Some(manifest) = build_info.manifest() else {
5765
// We can't proceed to checking (because that needs a full healthy manifest), and we've
5866
// already emitted any necessary warning, so quit.
@@ -89,8 +97,19 @@ pub async fn build(
8997
/// Run all component build commands, using the default options (build all
9098
/// components, perform target checking). We run a "default build" in several
9199
/// 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
100+
pub async fn build_default(
101+
manifest_file: &Path,
102+
profile: Option<&str>,
103+
cache_root: Option<PathBuf>,
104+
) -> Result<()> {
105+
build(
106+
manifest_file,
107+
profile,
108+
&[],
109+
TargetChecking::Check,
110+
cache_root,
111+
)
112+
.await
94113
}
95114

96115
fn build_components(
@@ -215,6 +234,69 @@ fn construct_workdir(app_dir: &Path, workdir: Option<impl AsRef<Path>>) -> Resul
215234
Ok(cwd)
216235
}
217236

237+
/// Saves the build profile to the "last build profile" file.
238+
pub fn save_last_build_profile(app_dir: &Path, profile: Option<&str>) -> anyhow::Result<()> {
239+
let app_stash_dir = app_dir.join(".spin");
240+
let last_build_profile_file = app_stash_dir.join(LAST_BUILD_PROFILE_FILE);
241+
242+
// This way, if the user never uses build profiles, they won't see a
243+
// weird savefile that they have no idea what it is.
244+
if profile.is_none() && !last_build_profile_file.exists() {
245+
return Ok(());
246+
}
247+
248+
std::fs::create_dir_all(&app_stash_dir)?;
249+
std::fs::write(
250+
&last_build_profile_file,
251+
profile.unwrap_or(LAST_BUILD_ANON_VALUE),
252+
)?;
253+
254+
Ok(())
255+
}
256+
257+
/// Reads the last build profile from the "last build profile" file.
258+
pub fn read_last_build_profile(app_dir: &Path) -> anyhow::Result<Option<String>> {
259+
let app_stash_dir = app_dir.join(".spin");
260+
let last_build_profile_file = app_stash_dir.join(LAST_BUILD_PROFILE_FILE);
261+
if !last_build_profile_file.exists() {
262+
return Ok(None);
263+
}
264+
265+
let last_build_str = std::fs::read_to_string(&last_build_profile_file)?;
266+
267+
if last_build_str == LAST_BUILD_ANON_VALUE {
268+
Ok(None)
269+
} else {
270+
Ok(Some(last_build_str))
271+
}
272+
}
273+
274+
/// Prints a warning to stderr if the given profile is not the same
275+
/// as the most recent build in the given application directory.
276+
pub fn warn_if_not_latest_build(manifest_path: &Path, profile: Option<&str>) {
277+
let Some(app_dir) = manifest_path.parent() else {
278+
return;
279+
};
280+
281+
let latest_build = match read_last_build_profile(app_dir) {
282+
Ok(profile) => profile,
283+
Err(e) => {
284+
tracing::warn!(
285+
"Failed to read last build profile: using anonymous profile. Error was {e:?}"
286+
);
287+
None
288+
}
289+
};
290+
291+
if profile != latest_build.as_deref() {
292+
let profile_opt = match profile {
293+
Some(p) => format!(" --profile {p}"),
294+
None => "".to_string(),
295+
};
296+
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}`.");
297+
}
298+
}
299+
218300
/// Specifies target environment checking behaviour
219301
pub enum TargetChecking {
220302
/// The build should check that all components are compatible with all target environments.
@@ -242,23 +324,23 @@ mod tests {
242324
#[tokio::test]
243325
async fn can_load_even_if_trigger_invalid() {
244326
let bad_trigger_file = test_data_root().join("bad_trigger.toml");
245-
build(&bad_trigger_file, &[], TargetChecking::Skip, None)
327+
build(&bad_trigger_file, None, &[], TargetChecking::Skip, None)
246328
.await
247329
.unwrap();
248330
}
249331

250332
#[tokio::test]
251333
async fn succeeds_if_target_env_matches() {
252334
let manifest_path = test_data_root().join("good_target_env.toml");
253-
build(&manifest_path, &[], TargetChecking::Check, None)
335+
build(&manifest_path, None, &[], TargetChecking::Check, None)
254336
.await
255337
.unwrap();
256338
}
257339

258340
#[tokio::test]
259341
async fn fails_if_target_env_does_not_match() {
260342
let manifest_path = test_data_root().join("bad_target_env.toml");
261-
let err = build(&manifest_path, &[], TargetChecking::Check, None)
343+
let err = build(&manifest_path, None, &[], TargetChecking::Check, None)
262344
.await
263345
.expect_err("should have failed")
264346
.to_string();
@@ -273,7 +355,8 @@ mod tests {
273355
#[tokio::test]
274356
async fn has_meaningful_error_if_target_env_does_not_match() {
275357
let manifest_file = test_data_root().join("bad_target_env.toml");
276-
let manifest = spin_manifest::manifest_from_file(&manifest_file).unwrap();
358+
let mut manifest = spin_manifest::manifest_from_file(&manifest_file).unwrap();
359+
spin_manifest::normalize::normalize_manifest(&mut manifest, None);
277360
let application = spin_environments::ApplicationToValidate::new(
278361
manifest.clone(),
279362
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: 40 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,41 @@ 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+
component.environment.extend(overrides.environment.clone());
138+
139+
component
140+
.dependencies
141+
.inner
142+
.extend(overrides.dependencies.inner.clone());
143+
}
144+
}

0 commit comments

Comments
 (0)