Skip to content

Commit bfc3957

Browse files
authored
Merge pull request #7852 from Extra-Chill/fix/7843-native-hydration-followup
Fix Lab hydration without dependency providers
2 parents 252ec42 + c43f3cf commit bfc3957

3 files changed

Lines changed: 83 additions & 19 deletions

File tree

src/core/deps.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,4 +561,35 @@ mod tests {
561561
);
562562
});
563563
}
564+
565+
#[test]
566+
fn dependency_install_plan_skips_linked_extensions_without_deps_support() {
567+
crate::test_support::with_isolated_home(|home| {
568+
let project = tempfile::tempdir().expect("project tempdir");
569+
let extension_id = "fixture-non-deps";
570+
let extension_dir = home
571+
.path()
572+
.join(".config/homeboy/extensions")
573+
.join(extension_id);
574+
std::fs::create_dir_all(&extension_dir).expect("extension dir");
575+
std::fs::write(
576+
extension_dir.join(format!("{extension_id}.json")),
577+
r#"{"name":"Fixture non-deps","version":"1.0.0"}"#,
578+
)
579+
.expect("extension manifest");
580+
std::fs::write(
581+
project.path().join("homeboy.json"),
582+
format!(
583+
r#"{{"id":"fixture","local_path":"{}","extensions":{{"{extension_id}":{{}}}}}}"#,
584+
project.path().display()
585+
),
586+
)
587+
.expect("component manifest");
588+
589+
let plan = dependency_install_plan(project.path())
590+
.expect("unrelated linked extensions do not require dependency hydration");
591+
592+
assert!(plan.is_empty());
593+
});
594+
}
564595
}

src/core/deps_provider.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -231,12 +231,12 @@ pub(crate) fn resolve_dependency_providers_optional(
231231
.map(|extensions| !extensions.is_empty())
232232
.unwrap_or(false)
233233
{
234-
match extension::resolve_execution_context(component, ExtensionCapability::Deps) {
235-
Ok(context) => providers.push(DependencyProvider::Extension(Box::new(
234+
if let Some(context) =
235+
extension::resolve_execution_context_if_available(component, ExtensionCapability::Deps)?
236+
{
237+
providers.push(DependencyProvider::Extension(Box::new(
236238
ExtensionDependencyProvider { context },
237-
))),
238-
Err(err) if providers.is_empty() => return Err(err),
239-
Err(_) => {}
239+
)));
240240
}
241241
}
242242

src/core/extension/capability.rs

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -279,15 +279,6 @@ fn explicit_capability_extension(
279279
.filter(|extension_id| !extension_id.is_empty())
280280
}
281281

282-
fn linked_extensions(
283-
component: &Component,
284-
) -> Result<&HashMap<String, crate::core::component::ScopedExtensionConfig>> {
285-
component
286-
.extensions
287-
.as_ref()
288-
.ok_or_else(|| no_extensions_error(component))
289-
}
290-
291282
pub fn extract_component_extension_settings(
292283
component: &Component,
293284
extension_id: &str,
@@ -310,9 +301,29 @@ pub fn resolve_extension_for_capability(
310301
component: &Component,
311302
capability: ExtensionCapability,
312303
) -> Result<String> {
313-
let extensions = linked_extensions(component)?;
304+
match resolve_extension_for_capability_if_available(component, capability)? {
305+
Some(extension_id) => Ok(extension_id),
306+
None if component
307+
.extensions
308+
.as_ref()
309+
.is_none_or(|extensions| extensions.is_empty()) => Err(no_extensions_error(component)),
310+
None => Err(capability_missing_error(component, capability)),
311+
}
312+
}
313+
314+
/// Resolve a capability only when one of the component's linked extensions
315+
/// advertises it. This lets optional consumers skip a capability that the
316+
/// component has not opted into without weakening validation of explicit or
317+
/// ambiguous capability ownership.
318+
fn resolve_extension_for_capability_if_available(
319+
component: &Component,
320+
capability: ExtensionCapability,
321+
) -> Result<Option<String>> {
322+
let Some(extensions) = component.extensions.as_ref() else {
323+
return Ok(None);
324+
};
314325
if extensions.is_empty() {
315-
return Err(no_extensions_error(component));
326+
return Ok(None);
316327
}
317328

318329
if let Some(extension_id) = explicit_capability_extension(component, capability) {
@@ -349,7 +360,7 @@ pub fn resolve_extension_for_capability(
349360
));
350361
}
351362

352-
return Ok(extension_id.to_string());
363+
return Ok(Some(extension_id.to_string()));
353364
}
354365

355366
let mut matching = Vec::new();
@@ -362,8 +373,8 @@ pub fn resolve_extension_for_capability(
362373
}
363374

364375
match matching.len() {
365-
0 => Err(capability_missing_error(component, capability)),
366-
1 => Ok(matching.remove(0)),
376+
0 => Ok(None),
377+
1 => Ok(Some(matching.remove(0))),
367378
_ => Err(capability_ambiguous_error(component, capability, &matching)),
368379
}
369380
}
@@ -373,6 +384,28 @@ pub fn resolve_execution_context(
373384
capability: ExtensionCapability,
374385
) -> Result<ExtensionExecutionContext> {
375386
let extension_id = resolve_extension_for_capability(component, capability)?;
387+
execution_context_for_extension(component, capability, extension_id)
388+
}
389+
390+
/// Resolve an execution context when a linked extension provides `capability`.
391+
/// A missing optional capability is represented as `Ok(None)`; malformed
392+
/// explicit ownership and ambiguous providers remain validation errors.
393+
pub(crate) fn resolve_execution_context_if_available(
394+
component: &Component,
395+
capability: ExtensionCapability,
396+
) -> Result<Option<ExtensionExecutionContext>> {
397+
let Some(extension_id) = resolve_extension_for_capability_if_available(component, capability)?
398+
else {
399+
return Ok(None);
400+
};
401+
execution_context_for_extension(component, capability, extension_id).map(Some)
402+
}
403+
404+
fn execution_context_for_extension(
405+
component: &Component,
406+
capability: ExtensionCapability,
407+
extension_id: String,
408+
) -> Result<ExtensionExecutionContext> {
376409
let manifest = load_extension(&extension_id)?;
377410
let script_path = capability
378411
.script_path(&manifest)

0 commit comments

Comments
 (0)