Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 76 additions & 7 deletions crates/build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,18 @@ use subprocess::{Exec, Redirection};

use crate::manifest::component_build_configs;

const LAST_BUILD_PROFILE_FILE: &str = "last-build.txt";
const LAST_BUILD_ANON_VALUE: &str = "<anonymous>";

/// If present, run the build command of each component.
pub async fn build(
manifest_file: &Path,
profile: Option<&str>,
component_ids: &[String],
target_checks: TargetChecking,
cache_root: Option<PathBuf>,
) -> Result<()> {
let build_info = component_build_configs(manifest_file)
let build_info = component_build_configs(manifest_file, profile)
.await
.with_context(|| {
format!(
Expand Down Expand Up @@ -53,6 +57,8 @@ pub async fn build(
// If the build failed, exit with an error at this point.
build_result?;

save_last_build_profile(&app_dir, profile);

let Some(manifest) = build_info.manifest() else {
// We can't proceed to checking (because that needs a full healthy manifest), and we've
// already emitted any necessary warning, so quit.
Expand All @@ -71,6 +77,7 @@ pub async fn build(
build_info.deployment_targets(),
cache_root.clone(),
&app_dir,
profile,
)
.await
.context("unable to check if the application is compatible with deployment targets")?;
Expand All @@ -89,8 +96,19 @@ pub async fn build(
/// Run all component build commands, using the default options (build all
/// components, perform target checking). We run a "default build" in several
/// places and this centralises the logic of what such a "default build" means.
pub async fn build_default(manifest_file: &Path, cache_root: Option<PathBuf>) -> Result<()> {
build(manifest_file, &[], TargetChecking::Check, cache_root).await
pub async fn build_default(
manifest_file: &Path,
profile: Option<&str>,
cache_root: Option<PathBuf>,
) -> Result<()> {
build(
manifest_file,
profile,
&[],
TargetChecking::Check,
cache_root,
)
.await
}

fn build_components(
Expand Down Expand Up @@ -148,7 +166,7 @@ fn build_component(build_info: ComponentBuildInfo, app_dir: &Path) -> Result<()>
);
}

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

/// Saves the build profile to the "last build profile" file.
/// Errors are ignored as they should not block building.
pub fn save_last_build_profile(app_dir: &Path, profile: Option<&str>) {
let app_stash_dir = app_dir.join(".spin");
_ = std::fs::create_dir_all(&app_stash_dir);
let last_build_profile_file = app_stash_dir.join(LAST_BUILD_PROFILE_FILE);
_ = std::fs::write(
&last_build_profile_file,
profile.unwrap_or(LAST_BUILD_ANON_VALUE),
);
}

/// Reads the last build profile from the "last build profile" file.
/// Errors are ignored.
pub fn read_last_build_profile(app_dir: &Path) -> Option<String> {
let app_stash_dir = app_dir.join(".spin");
let last_build_profile_file = app_stash_dir.join(LAST_BUILD_PROFILE_FILE);
let last_build_str = std::fs::read_to_string(&last_build_profile_file).ok()?;

if last_build_str == LAST_BUILD_ANON_VALUE {
None
} else {
Some(last_build_str)
}
}

/// Prints a warning to stderr if the given profile is not the same
/// as the most recent build in the given application directory.
pub fn warn_if_not_latest_build(manifest_path: &Path, profile: Option<&str>) {
let Some(app_dir) = manifest_path.parent() else {
return;
};

let latest_build = read_last_build_profile(app_dir);

let is_match = match (profile, latest_build) {
(None, None) => true,
(Some(_), None) | (None, Some(_)) => false,
(Some(p), Some(latest)) => p == latest,
};

if !is_match {
let profile_opt = match profile {
Some(p) => format!(" --profile {p}"),
None => "".to_string(),
};
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}`.");
}
}

/// Specifies target environment checking behaviour
pub enum TargetChecking {
/// The build should check that all components are compatible with all target environments.
Expand Down Expand Up @@ -242,23 +310,23 @@ mod tests {
#[tokio::test]
async fn can_load_even_if_trigger_invalid() {
let bad_trigger_file = test_data_root().join("bad_trigger.toml");
build(&bad_trigger_file, &[], TargetChecking::Skip, None)
build(&bad_trigger_file, None, &[], TargetChecking::Skip, None)
.await
.unwrap();
}

#[tokio::test]
async fn succeeds_if_target_env_matches() {
let manifest_path = test_data_root().join("good_target_env.toml");
build(&manifest_path, &[], TargetChecking::Check, None)
build(&manifest_path, None, &[], TargetChecking::Check, None)
.await
.unwrap();
}

#[tokio::test]
async fn fails_if_target_env_does_not_match() {
let manifest_path = test_data_root().join("bad_target_env.toml");
let err = build(&manifest_path, &[], TargetChecking::Check, None)
let err = build(&manifest_path, None, &[], TargetChecking::Check, None)
.await
.expect_err("should have failed")
.to_string();
Expand Down Expand Up @@ -287,6 +355,7 @@ mod tests {
&manifest.application.targets,
None,
manifest_file.parent().unwrap(),
None,
)
.await
.context("unable to check if the application is compatible with deployment targets")
Expand Down
12 changes: 9 additions & 3 deletions crates/build/src/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,17 @@ impl ManifestBuildInfo {
/// given (v1 or v2) manifest path. If the manifest cannot be loaded, the
/// function attempts fallback: if fallback succeeds, result is Ok but the load error
/// is also returned via the second part of the return value tuple.
pub async fn component_build_configs(manifest_file: impl AsRef<Path>) -> Result<ManifestBuildInfo> {
pub async fn component_build_configs(
manifest_file: impl AsRef<Path>,
profile: Option<&str>,
) -> Result<ManifestBuildInfo> {
let manifest = spin_manifest::manifest_from_file(&manifest_file);
match manifest {
Ok(mut manifest) => {
manifest.ensure_profile(profile)?;

spin_manifest::normalize::normalize_manifest(&mut manifest);
let components = build_configs_from_manifest(&manifest);
let components = build_configs_from_manifest(&manifest, profile);
let deployment_targets = deployment_targets_from_manifest(&manifest);
Ok(ManifestBuildInfo::Loadable {
components,
Expand Down Expand Up @@ -101,13 +106,14 @@ pub async fn component_build_configs(manifest_file: impl AsRef<Path>) -> Result<

fn build_configs_from_manifest(
manifest: &spin_manifest::schema::v2::AppManifest,
profile: Option<&str>,
) -> Vec<ComponentBuildInfo> {
manifest
.components
.iter()
.map(|(id, c)| ComponentBuildInfo {
id: id.to_string(),
build: c.build.clone(),
build: c.build(profile),
})
.collect()
}
Expand Down
7 changes: 3 additions & 4 deletions crates/doctor/src/rustlang/target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,9 @@ impl Diagnostic for TargetDiagnostic {
let manifest_str = patient.manifest_doc.to_string();
let manifest = spin_manifest::manifest_from_str(&manifest_str)?;
let uses_rust = manifest.components.values().any(|c| {
c.build
.as_ref()
.map(|b| b.commands().any(|c| c.starts_with("cargo")))
.unwrap_or_default()
c.build_commands(None)
.iter()
.any(|c| c.starts_with("cargo"))
});

if uses_rust {
Expand Down
6 changes: 3 additions & 3 deletions crates/doctor/src/wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ impl PatientWasm {
}

pub fn source_path(&self) -> Option<&Path> {
match &self.component.source {
match &self.component.source(None) {
v2::ComponentSource::Local(path) => Some(Path::new(path)),
_ => None,
}
}

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

pub fn has_build(&self) -> bool {
self.component.build.is_some()
self.component.build(None).is_some()
}
}

Expand Down
6 changes: 4 additions & 2 deletions crates/environments/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,14 @@ pub async fn validate_application_against_environment_ids(
env_ids: &[TargetEnvironmentRef],
cache_root: Option<std::path::PathBuf>,
app_dir: &std::path::Path,
profile: Option<&str>,
) -> anyhow::Result<TargetEnvironmentValidation> {
if env_ids.is_empty() {
return Ok(Default::default());
}

let envs = TargetEnvironment::load_all(env_ids, cache_root, app_dir).await?;
validate_application_against_environments(application, &envs).await
validate_application_against_environments(application, &envs, profile).await
}

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

let components_by_trigger_type = application.components_by_trigger_type().await?;
let components_by_trigger_type = application.components_by_trigger_type(profile).await?;

let mut errs = vec![];

Expand Down
20 changes: 13 additions & 7 deletions crates/environments/src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ impl ApplicationToValidate {
fn component_source<'a>(
&'a self,
trigger: &'a spin_manifest::schema::v2::Trigger,
profile: Option<&str>,
) -> anyhow::Result<ComponentSource<'a>> {
let component_spec = trigger
.component
Expand All @@ -75,8 +76,8 @@ impl ApplicationToValidate {
let (id, source, dependencies, service_chaining) = match component_spec {
spin_manifest::schema::v2::ComponentSpec::Inline(c) => (
trigger.id.as_str(),
&c.source,
&c.dependencies,
&c.source(profile),
&c.dependencies(profile),
spin_loader::requires_service_chaining(c),
),
spin_manifest::schema::v2::ComponentSpec::Reference(r) => {
Expand All @@ -89,8 +90,8 @@ impl ApplicationToValidate {
};
(
id,
&component.source,
&component.dependencies,
&component.source(profile),
&component.dependencies(profile),
spin_loader::requires_service_chaining(component),
)
}
Expand All @@ -116,11 +117,12 @@ impl ApplicationToValidate {

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

let components_by_trigger_type_futs = self.triggers().map(|(ty, ts)| {
self.components_for_trigger(ts)
self.components_for_trigger(ts, profile)
.map(|css| css.map(|css| (ty.to_owned(), css)))
});
let components_by_trigger_type = try_join_all(components_by_trigger_type_futs)
Expand All @@ -132,16 +134,20 @@ impl ApplicationToValidate {
async fn components_for_trigger<'a>(
&'a self,
triggers: &'a [spin_manifest::schema::v2::Trigger],
profile: Option<&str>,
) -> anyhow::Result<Vec<ComponentToValidate<'a>>> {
let component_futures = triggers.iter().map(|t| self.load_and_resolve_trigger(t));
let component_futures = triggers
.iter()
.map(|t| self.load_and_resolve_trigger(t, profile));
try_join_all(component_futures).await
}

async fn load_and_resolve_trigger<'a>(
&'a self,
trigger: &'a spin_manifest::schema::v2::Trigger,
profile: Option<&str>,
) -> anyhow::Result<ComponentToValidate<'a>> {
let component = self.component_source(trigger)?;
let component = self.component_source(trigger, profile)?;

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

Expand Down
2 changes: 1 addition & 1 deletion crates/factors-test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,5 +102,5 @@ pub async fn build_locked_app(manifest: &toml::Table) -> anyhow::Result<LockedAp
let dir = tempfile::tempdir().context("failed creating tempdir")?;
let path = dir.path().join("spin.toml");
std::fs::write(&path, toml_str).context("failed writing manifest")?;
spin_loader::from_file(&path, FilesMountStrategy::Direct, None).await
spin_loader::from_file(&path, FilesMountStrategy::Direct, None, None).await
}
5 changes: 3 additions & 2 deletions crates/loader/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,20 @@ pub(crate) const MAX_FILE_LOADING_CONCURRENCY: usize = 16;
pub async fn from_file(
manifest_path: impl AsRef<Path>,
files_mount_strategy: FilesMountStrategy,
profile: Option<&str>,
cache_root: Option<PathBuf>,
) -> Result<LockedApp> {
let path = manifest_path.as_ref();
let app_root = parent_dir(path).context("manifest path has no parent directory")?;
let loader = LocalLoader::new(&app_root, files_mount_strategy, cache_root).await?;
let loader = LocalLoader::new(&app_root, files_mount_strategy, profile, cache_root).await?;
loader.load_file(path).await
}

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

Expand Down
Loading
Loading