Skip to content

Commit 163503d

Browse files
committed
Updated after initial review
Signed-off-by: itowlson <[email protected]>
1 parent ef342c8 commit 163503d

File tree

8 files changed

+162
-80
lines changed

8 files changed

+162
-80
lines changed

Cargo.lock

-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/build/src/lib.rs

+41-24
Original file line numberDiff line numberDiff line change
@@ -21,42 +21,59 @@ pub async fn build(
2121
component_ids: &[String],
2222
skip_target_checks: bool,
2323
) -> Result<()> {
24-
let (components, deployment_targets, manifest) = component_build_configs(manifest_file)
24+
let build_info = component_build_configs(manifest_file)
2525
.await
2626
.with_context(|| {
27-
format!(
28-
"Cannot read manifest file from {}",
29-
quoted_path(manifest_file)
30-
)
31-
})?;
27+
format!(
28+
"Cannot read manifest file from {}",
29+
quoted_path(manifest_file)
30+
)
31+
})?;
3232
let app_dir = parent_dir(manifest_file)?;
3333

34-
let build_result = build_components(component_ids, components, app_dir);
34+
let build_result = build_components(component_ids, build_info.components(), app_dir);
3535

36-
if let Err(e) = &manifest {
36+
// Emit any required warnings now, so that they don't bury any errors.
37+
if let Some(e) = build_info.load_error() {
38+
// The manifest had errors. We managed to attempt a build anyway, but we want to
39+
// let the user know about them.
3740
terminal::warn!("The manifest has errors not related to the Wasm component build. Error details:\n{e:#}");
41+
// Checking deployment targets requires a healthy manifest (because trigger types etc.),
42+
// if any of these were specified, warn they are being skipped.
43+
let should_have_checked_targets =
44+
!skip_target_checks && build_info.has_deployment_targets();
45+
if should_have_checked_targets {
46+
terminal::warn!(
47+
"The manifest error(s) prevented Spin from checking the deployment targets."
48+
);
49+
}
3850
}
3951

52+
// If the build failed, exit with an error at this point.
4053
build_result?;
4154

42-
if let Ok(manifest) = &manifest {
43-
if !skip_target_checks {
44-
let resolution_context =
45-
spin_environments::ResolutionContext::new(manifest_file.parent().unwrap()).await?;
46-
let errors = spin_environments::validate_application_against_environment_ids(
47-
&deployment_targets,
48-
manifest,
49-
&resolution_context,
50-
)
51-
.await?;
55+
let Some(manifest) = build_info.manifest() else {
56+
// We can't proceed to checking (because that needs a full healthy manifest), and we've
57+
// already emitted any necessary warning, so quit.
58+
return Ok(());
59+
};
5260

53-
for error in &errors {
54-
terminal::error!("{error}");
55-
}
61+
if !skip_target_checks {
62+
let resolution_context =
63+
spin_environments::ResolutionContext::new(manifest_file.parent().unwrap()).await?;
64+
let errors = spin_environments::validate_application_against_environment_ids(
65+
build_info.deployment_targets(),
66+
manifest,
67+
&resolution_context,
68+
)
69+
.await?;
5670

57-
if !errors.is_empty() {
58-
anyhow::bail!("All components built successfully, but one or more was incompatible with one or more of the deployment targets.");
59-
}
71+
for error in &errors {
72+
terminal::error!("{error}");
73+
}
74+
75+
if !errors.is_empty() {
76+
anyhow::bail!("All components built successfully, but one or more was incompatible with one or more of the deployment targets.");
6077
}
6178
}
6279

crates/build/src/manifest.rs

+86-30
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,96 @@ use std::{collections::BTreeMap, path::Path};
44

55
use spin_manifest::{schema::v2, ManifestVersion};
66

7-
pub type DeploymentTarget = String;
8-
pub type DeploymentTargets = Vec<DeploymentTarget>;
7+
pub enum ManifestBuildInfo {
8+
Loadable {
9+
components: Vec<ComponentBuildInfo>,
10+
deployment_targets: Vec<String>,
11+
manifest: spin_manifest::schema::v2::AppManifest,
12+
},
13+
Unloadable {
14+
components: Vec<ComponentBuildInfo>,
15+
has_deployment_targets: bool,
16+
load_error: spin_manifest::Error,
17+
},
18+
}
19+
20+
impl ManifestBuildInfo {
21+
pub fn components(&self) -> Vec<ComponentBuildInfo> {
22+
match self {
23+
Self::Loadable { components, .. } => components.clone(),
24+
Self::Unloadable { components, .. } => components.clone(),
25+
}
26+
}
27+
28+
pub fn load_error(&self) -> Option<&spin_manifest::Error> {
29+
match self {
30+
Self::Loadable { .. } => None,
31+
Self::Unloadable { load_error, .. } => Some(load_error),
32+
}
33+
}
34+
35+
pub fn deployment_targets(&self) -> &[String] {
36+
match self {
37+
Self::Loadable {
38+
deployment_targets, ..
39+
} => deployment_targets,
40+
Self::Unloadable { .. } => &[],
41+
}
42+
}
43+
44+
pub fn has_deployment_targets(&self) -> bool {
45+
match self {
46+
Self::Loadable {
47+
deployment_targets, ..
48+
} => !deployment_targets.is_empty(),
49+
Self::Unloadable {
50+
has_deployment_targets,
51+
..
52+
} => *has_deployment_targets,
53+
}
54+
}
55+
56+
pub fn manifest(&self) -> Option<&spin_manifest::schema::v2::AppManifest> {
57+
match self {
58+
Self::Loadable { manifest, .. } => Some(manifest),
59+
Self::Unloadable { .. } => None,
60+
}
61+
}
62+
}
963

1064
/// Returns a map of component IDs to [`v2::ComponentBuildConfig`]s for the
1165
/// given (v1 or v2) manifest path. If the manifest cannot be loaded, the
1266
/// function attempts fallback: if fallback succeeds, result is Ok but the load error
1367
/// is also returned via the second part of the return value tuple.
14-
pub async fn component_build_configs(
15-
manifest_file: impl AsRef<Path>,
16-
) -> Result<(
17-
Vec<ComponentBuildInfo>,
18-
DeploymentTargets,
19-
Result<spin_manifest::schema::v2::AppManifest, spin_manifest::Error>,
20-
)> {
68+
pub async fn component_build_configs(manifest_file: impl AsRef<Path>) -> Result<ManifestBuildInfo> {
2169
let manifest = spin_manifest::manifest_from_file(&manifest_file);
2270
match manifest {
2371
Ok(mut manifest) => {
2472
spin_manifest::normalize::normalize_manifest(&mut manifest);
25-
let bc = build_configs_from_manifest(&manifest);
26-
let dt = deployment_targets_from_manifest(&manifest);
27-
Ok((bc, dt, Ok(manifest)))
73+
let components = build_configs_from_manifest(&manifest);
74+
let deployment_targets = deployment_targets_from_manifest(&manifest);
75+
Ok(ManifestBuildInfo::Loadable {
76+
components,
77+
deployment_targets,
78+
manifest,
79+
})
2880
}
29-
Err(e) => {
30-
let bc = fallback_load_build_configs(&manifest_file).await?;
31-
let dt = fallback_load_deployment_targets(&manifest_file).await?;
32-
Ok((bc, dt, Err(e)))
81+
Err(load_error) => {
82+
// The manifest didn't load, but the problem might not be build-affecting.
83+
// Try to fall back by parsing out only the bits we need. And if something
84+
// goes wrong with the fallback, give up and return the original manifest load
85+
// error.
86+
let Ok(components) = fallback_load_build_configs(&manifest_file).await else {
87+
return Err(load_error.into());
88+
};
89+
let Ok(has_deployment_targets) = has_deployment_targets(&manifest_file).await else {
90+
return Err(load_error.into());
91+
};
92+
Ok(ManifestBuildInfo::Unloadable {
93+
components,
94+
has_deployment_targets,
95+
load_error,
96+
})
3397
}
3498
}
3599
}
@@ -49,7 +113,7 @@ fn build_configs_from_manifest(
49113

50114
fn deployment_targets_from_manifest(
51115
manifest: &spin_manifest::schema::v2::AppManifest,
52-
) -> DeploymentTargets {
116+
) -> Vec<String> {
53117
manifest.application.targets.clone()
54118
}
55119

@@ -75,31 +139,23 @@ async fn fallback_load_build_configs(
75139
})
76140
}
77141

78-
async fn fallback_load_deployment_targets(
79-
manifest_file: impl AsRef<Path>,
80-
) -> Result<DeploymentTargets> {
142+
async fn has_deployment_targets(manifest_file: impl AsRef<Path>) -> Result<bool> {
81143
let manifest_text = tokio::fs::read_to_string(manifest_file).await?;
82144
Ok(match ManifestVersion::detect(&manifest_text)? {
83-
ManifestVersion::V1 => Default::default(),
145+
ManifestVersion::V1 => false,
84146
ManifestVersion::V2 => {
85147
let table: toml::value::Table = toml::from_str(&manifest_text)?;
86-
let target_environments = table
148+
table
87149
.get("application")
88150
.and_then(|a| a.as_table())
89151
.and_then(|t| t.get("targets"))
90152
.and_then(|arr| arr.as_array())
91-
.map(|v| v.as_slice())
92-
.unwrap_or_default()
93-
.iter()
94-
.filter_map(|t| t.as_str())
95-
.map(|s| s.to_owned())
96-
.collect();
97-
target_environments
153+
.is_some_and(|arr| !arr.is_empty())
98154
}
99155
})
100156
}
101157

102-
#[derive(Deserialize)]
158+
#[derive(Clone, Deserialize)]
103159
pub struct ComponentBuildInfo {
104160
#[serde(default)]
105161
pub id: String,

crates/compose/src/lib.rs

+4
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ pub async fn compose<'a, L: ComponentSourceLoader>(
3333
Composer::new(loader).compose(component).await
3434
}
3535

36+
/// A Spin component dependency. This abstracts over the metadata associated with the
37+
/// dependency. The abstraction allows both manifest and lockfile types to participate in composition.
3638
#[async_trait::async_trait]
3739
pub trait DependencyLike {
3840
fn inherit(&self) -> InheritConfiguration;
@@ -44,6 +46,8 @@ pub enum InheritConfiguration {
4446
Some(Vec<String>),
4547
}
4648

49+
/// A Spin component. This abstracts over the list of dependencies for the component.
50+
/// The abstraction allows both manifest and lockfile types to participate in composition.
4751
#[async_trait::async_trait]
4852
pub trait ComponentLike {
4953
type Dependency: DependencyLike;

crates/environments/Cargo.toml

+4-5
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,11 @@ bytes = "1.1"
1111
futures = "0.3"
1212
futures-util = "0.3"
1313
id-arena = "2"
14-
indexmap = "2.2.6"
15-
miette = "7.2.0"
14+
indexmap = "2"
1615
oci-distribution = { git = "https://github.com/fermyon/oci-distribution", rev = "7e4ce9be9bcd22e78a28f06204931f10c44402ba" }
17-
semver = "1.0"
18-
serde = { version = "1.0", features = ["derive"] }
19-
serde_json = "1.0"
16+
semver = "1"
17+
serde = { version = "1", features = ["derive"] }
18+
serde_json = "1"
2019
spin-common = { path = "../common" }
2120
spin-componentize = { path = "../componentize" }
2221
spin-compose = { path = "../compose" }

crates/environments/src/environment_definition.rs

+22-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use anyhow::Context;
22

3+
/// Loads the given `TargetEnvironment` from a registry.
34
pub async fn load_environment(env_id: impl AsRef<str>) -> anyhow::Result<TargetEnvironment> {
45
use futures_util::TryStreamExt;
56

@@ -37,10 +38,20 @@ pub async fn load_environment(env_id: impl AsRef<str>) -> anyhow::Result<TargetE
3738
TargetEnvironment::new(env_id.to_owned(), bytes)
3839
}
3940

41+
/// A parsed document representing a deployment environment, e.g. Spin 2.7,
42+
/// SpinKube 3.1, Fermyon Cloud. The `TargetEnvironment` provides a mapping
43+
/// from the Spin trigger types supported in the environment to the Component Model worlds
44+
/// supported by that trigger type. (A trigger type may support more than one world,
45+
/// for example when it supports multiple versions of the Spin or WASI interfaces.)
46+
///
47+
/// In terms of implementation, internally the environment is represented by a
48+
/// WIT package that adheres to a specific naming convention (that the worlds for
49+
/// a given trigger type are exactly whose names begin `trigger-xxx` where
50+
/// `xxx` is the Spin trigger type).
4051
pub struct TargetEnvironment {
4152
name: String,
4253
decoded: wit_parser::decoding::DecodedWasm,
43-
package: wit_parser::Package, // saves unwrapping it every time
54+
package: wit_parser::Package,
4455
package_id: id_arena::Id<wit_parser::Package>,
4556
package_bytes: Vec<u8>,
4657
}
@@ -68,11 +79,14 @@ impl TargetEnvironment {
6879
})
6980
}
7081

82+
/// Returns true if the given trigger type provides the world identified by
83+
/// `world` in this environment.
7184
pub fn is_world_for(&self, trigger_type: &TriggerType, world: &wit_parser::World) -> bool {
7285
world.name.starts_with(&format!("trigger-{trigger_type}"))
7386
&& world.package.is_some_and(|p| p == self.package_id)
7487
}
7588

89+
/// Returns true if the given trigger type can run in this environment.
7690
pub fn supports_trigger_type(&self, trigger_type: &TriggerType) -> bool {
7791
self.decoded
7892
.resolve()
@@ -81,6 +95,7 @@ impl TargetEnvironment {
8195
.any(|(_, world)| self.is_world_for(trigger_type, world))
8296
}
8397

98+
/// Lists all worlds supported for the given trigger type in this environment.
8499
pub fn worlds(&self, trigger_type: &TriggerType) -> Vec<String> {
85100
self.decoded
86101
.resolve()
@@ -93,10 +108,10 @@ impl TargetEnvironment {
93108

94109
/// Fully qualified world name (e.g. fermyon:spin/[email protected])
95110
fn world_qname(&self, world: &wit_parser::World) -> String {
96-
let version_suffix = match self.package_version() {
97-
Some(version) => format!("@{version}"),
98-
None => "".to_owned(),
99-
};
111+
let version_suffix = self
112+
.package_version()
113+
.map(|version| format!("@{version}"))
114+
.unwrap_or_default();
100115
format!(
101116
"{}/{}{version_suffix}",
102117
self.package_namespaced_name(),
@@ -114,10 +129,12 @@ impl TargetEnvironment {
114129
format!("{}:{}", self.package.name.namespace, self.package.name.name)
115130
}
116131

132+
/// The package version for the environment package.
117133
pub fn package_version(&self) -> Option<&semver::Version> {
118134
self.package.name.version.as_ref()
119135
}
120136

137+
/// The Wasm-encoded bytes of the environment package.
121138
pub fn package_bytes(&self) -> &[u8] {
122139
&self.package_bytes
123140
}

0 commit comments

Comments
 (0)