Skip to content

Commit ea595fc

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

File tree

24 files changed

+636
-71
lines changed

24 files changed

+636
-71
lines changed

crates/build/src/lib.rs

Lines changed: 76 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.
@@ -71,6 +77,7 @@ pub async fn build(
7177
build_info.deployment_targets(),
7278
cache_root.clone(),
7379
&app_dir,
80+
profile,
7481
)
7582
.await
7683
.context("unable to check if the application is compatible with deployment targets")?;
@@ -89,8 +96,19 @@ pub async fn build(
8996
/// Run all component build commands, using the default options (build all
9097
/// components, perform target checking). We run a "default build" in several
9198
/// 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
99+
pub async fn build_default(
100+
manifest_file: &Path,
101+
profile: Option<&str>,
102+
cache_root: Option<PathBuf>,
103+
) -> Result<()> {
104+
build(
105+
manifest_file,
106+
profile,
107+
&[],
108+
TargetChecking::Check,
109+
cache_root,
110+
)
111+
.await
94112
}
95113

96114
fn build_components(
@@ -148,7 +166,7 @@ fn build_component(build_info: ComponentBuildInfo, app_dir: &Path) -> Result<()>
148166
);
149167
}
150168

151-
for (index, command) in b.commands().enumerate() {
169+
for (index, command) in b.commands().iter().enumerate() {
152170
if command_count > 1 {
153171
terminal::step!(
154172
"Running build step",
@@ -215,6 +233,56 @@ fn construct_workdir(app_dir: &Path, workdir: Option<impl AsRef<Path>>) -> Resul
215233
Ok(cwd)
216234
}
217235

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

250318
#[tokio::test]
251319
async fn succeeds_if_target_env_matches() {
252320
let manifest_path = test_data_root().join("good_target_env.toml");
253-
build(&manifest_path, &[], TargetChecking::Check, None)
321+
build(&manifest_path, None, &[], TargetChecking::Check, None)
254322
.await
255323
.unwrap();
256324
}
257325

258326
#[tokio::test]
259327
async fn fails_if_target_env_does_not_match() {
260328
let manifest_path = test_data_root().join("bad_target_env.toml");
261-
let err = build(&manifest_path, &[], TargetChecking::Check, None)
329+
let err = build(&manifest_path, None, &[], TargetChecking::Check, None)
262330
.await
263331
.expect_err("should have failed")
264332
.to_string();
@@ -287,6 +355,7 @@ mod tests {
287355
&manifest.application.targets,
288356
None,
289357
manifest_file.parent().unwrap(),
358+
None,
290359
)
291360
.await
292361
.context("unable to check if the application is compatible with deployment targets")

crates/build/src/manifest.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,17 @@ 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) => {
76+
manifest.ensure_profile(profile)?;
77+
7378
spin_manifest::normalize::normalize_manifest(&mut manifest);
74-
let components = build_configs_from_manifest(&manifest);
79+
let components = build_configs_from_manifest(&manifest, profile);
7580
let deployment_targets = deployment_targets_from_manifest(&manifest);
7681
Ok(ManifestBuildInfo::Loadable {
7782
components,
@@ -101,13 +106,14 @@ pub async fn component_build_configs(manifest_file: impl AsRef<Path>) -> Result<
101106

102107
fn build_configs_from_manifest(
103108
manifest: &spin_manifest::schema::v2::AppManifest,
109+
profile: Option<&str>,
104110
) -> Vec<ComponentBuildInfo> {
105111
manifest
106112
.components
107113
.iter()
108114
.map(|(id, c)| ComponentBuildInfo {
109115
id: id.to_string(),
110-
build: c.build.clone(),
116+
build: c.build(profile),
111117
})
112118
.collect()
113119
}

crates/doctor/src/rustlang/target.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,9 @@ impl Diagnostic for TargetDiagnostic {
1717
let manifest_str = patient.manifest_doc.to_string();
1818
let manifest = spin_manifest::manifest_from_str(&manifest_str)?;
1919
let uses_rust = manifest.components.values().any(|c| {
20-
c.build
21-
.as_ref()
22-
.map(|b| b.commands().any(|c| c.starts_with("cargo")))
23-
.unwrap_or_default()
20+
c.build_commands(None)
21+
.iter()
22+
.any(|c| c.starts_with("cargo"))
2423
});
2524

2625
if uses_rust {

crates/doctor/src/wasm.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,14 @@ impl PatientWasm {
3333
}
3434

3535
pub fn source_path(&self) -> Option<&Path> {
36-
match &self.component.source {
36+
match &self.component.source(None) {
3737
v2::ComponentSource::Local(path) => Some(Path::new(path)),
3838
_ => None,
3939
}
4040
}
4141

4242
pub fn abs_source_path(&self) -> Option<PathBuf> {
43-
match &self.component.source {
43+
match &self.component.source(None) {
4444
v2::ComponentSource::Local(path) => {
4545
// TODO: We probably need a doctor check to see if the path can be expanded!
4646
// For now, fall back to the literal path.
@@ -54,7 +54,7 @@ impl PatientWasm {
5454
}
5555

5656
pub fn has_build(&self) -> bool {
57-
self.component.build.is_some()
57+
self.component.build(None).is_some()
5858
}
5959
}
6060

crates/environments/src/lib.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,14 @@ pub async fn validate_application_against_environment_ids(
3939
env_ids: &[TargetEnvironmentRef],
4040
cache_root: Option<std::path::PathBuf>,
4141
app_dir: &std::path::Path,
42+
profile: Option<&str>,
4243
) -> anyhow::Result<TargetEnvironmentValidation> {
4344
if env_ids.is_empty() {
4445
return Ok(Default::default());
4546
}
4647

4748
let envs = TargetEnvironment::load_all(env_ids, cache_root, app_dir).await?;
48-
validate_application_against_environments(application, &envs).await
49+
validate_application_against_environments(application, &envs, profile).await
4950
}
5051

5152
/// Validates *all* application components against the list of (realised) target enviroments. Each component must conform
@@ -55,6 +56,7 @@ pub async fn validate_application_against_environment_ids(
5556
async fn validate_application_against_environments(
5657
application: &ApplicationToValidate,
5758
envs: &[TargetEnvironment],
59+
profile: Option<&str>,
5860
) -> anyhow::Result<TargetEnvironmentValidation> {
5961
for trigger_type in application.trigger_types() {
6062
if let Some(env) = envs.iter().find(|e| !e.supports_trigger_type(trigger_type)) {
@@ -65,7 +67,7 @@ async fn validate_application_against_environments(
6567
}
6668
}
6769

68-
let components_by_trigger_type = application.components_by_trigger_type().await?;
70+
let components_by_trigger_type = application.components_by_trigger_type(profile).await?;
6971

7072
let mut errs = vec![];
7173

crates/environments/src/loader.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ impl ApplicationToValidate {
6767
fn component_source<'a>(
6868
&'a self,
6969
trigger: &'a spin_manifest::schema::v2::Trigger,
70+
profile: Option<&str>,
7071
) -> anyhow::Result<ComponentSource<'a>> {
7172
let component_spec = trigger
7273
.component
@@ -75,8 +76,8 @@ impl ApplicationToValidate {
7576
let (id, source, dependencies, service_chaining) = match component_spec {
7677
spin_manifest::schema::v2::ComponentSpec::Inline(c) => (
7778
trigger.id.as_str(),
78-
&c.source,
79-
&c.dependencies,
79+
&c.source(profile),
80+
&c.dependencies(profile),
8081
spin_loader::requires_service_chaining(c),
8182
),
8283
spin_manifest::schema::v2::ComponentSpec::Reference(r) => {
@@ -89,8 +90,8 @@ impl ApplicationToValidate {
8990
};
9091
(
9192
id,
92-
&component.source,
93-
&component.dependencies,
93+
&component.source(profile),
94+
&component.dependencies(profile),
9495
spin_loader::requires_service_chaining(component),
9596
)
9697
}
@@ -116,11 +117,12 @@ impl ApplicationToValidate {
116117

117118
pub(crate) async fn components_by_trigger_type(
118119
&self,
120+
profile: Option<&str>,
119121
) -> anyhow::Result<Vec<(String, Vec<ComponentToValidate<'_>>)>> {
120122
use futures::FutureExt;
121123

122124
let components_by_trigger_type_futs = self.triggers().map(|(ty, ts)| {
123-
self.components_for_trigger(ts)
125+
self.components_for_trigger(ts, profile)
124126
.map(|css| css.map(|css| (ty.to_owned(), css)))
125127
});
126128
let components_by_trigger_type = try_join_all(components_by_trigger_type_futs)
@@ -132,16 +134,20 @@ impl ApplicationToValidate {
132134
async fn components_for_trigger<'a>(
133135
&'a self,
134136
triggers: &'a [spin_manifest::schema::v2::Trigger],
137+
profile: Option<&str>,
135138
) -> anyhow::Result<Vec<ComponentToValidate<'a>>> {
136-
let component_futures = triggers.iter().map(|t| self.load_and_resolve_trigger(t));
139+
let component_futures = triggers
140+
.iter()
141+
.map(|t| self.load_and_resolve_trigger(t, profile));
137142
try_join_all(component_futures).await
138143
}
139144

140145
async fn load_and_resolve_trigger<'a>(
141146
&'a self,
142147
trigger: &'a spin_manifest::schema::v2::Trigger,
148+
profile: Option<&str>,
143149
) -> anyhow::Result<ComponentToValidate<'a>> {
144-
let component = self.component_source(trigger)?;
150+
let component = self.component_source(trigger, profile)?;
145151

146152
let loader = ComponentSourceLoader::new(&self.wasm_loader);
147153

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

0 commit comments

Comments
 (0)