8000 chore(sessions): don't compose lifecycle hooks until we finish implementing them by evanspearman · Pull Request #975 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content
Merged
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
8 changes: 8 additions & 0 deletions crates/mfile/src/project_composable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ impl Composable for ProjectComposable {
let source = Source::Project {
path: self.project_path,
};
// Session transition scripts declared in a project `[session]`
// block are accepted and composed like any other primitive; the
// feature is gated off for this release purely at the output and
// execution layers (see `crates/minimald/src/session_host.rs`),
// not by dropping them here.
contribute_primitives(
&source,
self.session.packages,
Expand Down Expand Up @@ -114,6 +119,9 @@ mod tests {
assert_eq!(contribution.vars().len(), 2);
assert_eq!(contribution.patches().len(), 1);
assert_eq!(contribution.packages().len(), 2);
// A declared project hook is accepted and composed — the feature
// is disabled downstream (output + execution), not by dropping it
// here.
assert_eq!(contribution.lifecycle_hooks().len(), 1);

// Provenance is Source::Project everywhere, carrying the
Expand Down
53 changes: 51 additions & 2 deletions crates/minimal/src/loadouts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,16 @@ pub(crate) fn compose_user_contribution(
),
anyhow::Error,
> {
// Session transition scripts declared in a loadout are not available
// in this release, so they are silently excluded here — before the
// loadouts reach the composer — rather than after composition. This
// keeps a declared hook from participating in composition at all.
// Drop the `without_lifecycle_hooks` map when the feature ships.
let loadouts: Vec<_> = loadouts
.into_iter()
.map(sessions::core::loadout::Loadout::without_lifecycle_hooks)
.collect();

let mut composer = sessions::client::composer::UserComposer::new();
composer
.add_all(loadouts)
Expand Down Expand Up @@ -232,11 +242,10 @@ impl LoadoutRow {
name: entry.file_stem.clone(),
desc: l.description().unwrap_or("").to_string(),
counts: format!(
"{} pkg / {} var / {} patch / {} hook",
"{} pkg / {} var / {} patch",
l.packages().len(),
l.vars().len() + l.vars_lenient().len(),
l.patches().iter().count(),
l.lifecycle_hooks().len(),
),
},
Err(e) => Self {
Expand Down Expand Up @@ -313,4 +322,44 @@ mod tests {
let selection = LoadoutSelection::Cli(vec!["missing".to_string()]);
assert!(resolve_active_loadouts(selection, &cfg, &global).is_err());
}

/// A loadout that declares a session transition script still composes
/// cleanly: the hook is excluded before composition (see
/// [`compose_user_contribution`]), so it neither errors nor lands in
/// the wire contribution, while the loadout's other items compose
/// normally.
#[test]
fn loadout_lifecycle_hook_is_excluded_without_affecting_composition() {
let loadout: sessions::core::loadout::Loadout = toml::from_str(
r#"
name = "dev"
packages = ["helix"]

[vars]
EDITOR = "hx"

[[lifecycle_hooks]]
on_activate = { type = "inline", value = "echo activated" }
"#,
)
.expect("loadout parses");
// Sanity: the hook really is present on the loadout we feed in.
assert_eq!(loadout.lifecycle_hooks().len(), 1);

let (wire, _policy) = compose_user_contribution(
vec![loadout],
sessions::core::policy::UserPolicy::empty(),
sessions::core::compose::ComposeOptions::default(),
)
.expect("composition succeeds despite the declared hook");

// The declared hook is silently excluded...
assert!(
wire.lifecycle_hooks.is_empty(),
"declared lifecycle hook must be excluded from the contribution"
);
// ...while the loadout's other items compose normally.
assert_eq!(wire.requested_packages.len(), 1);
assert_eq!(wire.vars.len(), 1);
}
}
64 changes: 34 additions & 30 deletions crates/minimald/src/session_host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,10 @@ impl Pty {
}

/// Emit one `tracing::info!` per item the launcher folds into the
/// session — packages, vars, patches, and lifecycle hooks —
/// tagging each with its provenance so an operator can trace
/// "where did `EDITOR=hx` come from?" back to the loadout /
/// project / package that contributed it.
/// session — packages, vars, and patches — tagging each with its
/// provenance so an operator can trace "where did `EDITOR=hx` come
/// from?" back to the loadout / project / package that contributed
/// it.
///
/// Baseline items (the launcher-defaults `PS1`, `base`, `coreutils`,
/// `socat`) log with `source = "launcher-baseline"` so they can be
Expand Down Expand Up @@ -241,32 +241,36 @@ fn log_session_contents(
"session content (patch: materialized into session home at FinalizeSession)",
);
}
for h in comp.lifecycle_hooks() {
let src = sessions::core::source::Provenanced::source(h);
let hook = h.hook();
[
("on_activate", hook.on_activate()),
("on_destroy", hook.on_destroy()),
("on_failure", hook.on_failure()),
]
.into_iter()
.filter_map(|(event, script)| script.map(|s| (event, s)))
.for_each(|(event, script)| {
let kind = match script {
sessions::core::lifecyclehook::HookScript::Inline(_) => "inline",
sessions::core::lifecyclehook::HookScript::External(_) => "external",
};
tracing::info!(
session = session_name,
domain = "lifecycle_hook",
event = event,
script_kind = kind,
source = ?src,
deferred = true,
"session content (lifecycle hook: exec plumbing deferred)",
);
});
}
// Session transition scripts are gated off for this release. A hook
// declared in a project `[session]` block is still accepted and
// composed into the session — it is disabled here, at the output
// layer, by suppressing the content-logging loop below (and it is
// never executed, as exec is deferred), rather than being refused at
// compose time. Restore the loop when the feature ships. (Hooks from
// loadouts are excluded earlier, before client-side composition — see
// `crates/minimal/src/loadouts.rs`.)
//
// for h in comp.lifecycle_hooks() {
// let src = sessions::core::source::Provenanced::source(h);
// let hook = h.hook();
// [
// ("on_activate", hook.on_activate()),
// ("on_destroy", hook.on_destroy()),
// ("on_failure", hook.on_failure()),
// ]
// .into_iter()
// .filter_map(|(event, script)| script.map(|s| (event, s)))
// .for_each(|(event, script)| {
// let kind = match script {
// sessions::core::lifecyclehook::HookScript::Inline(_) => "inline",
// sessions::core::lifecyclehook::HookScript::External(_) => "external",
// };
// tracing::info!(
// session = session_name,
// ...
// );
// });
// }
}

/// Duplicate `fd` into a new close-on-exec `OwnedFd` via
Expand Down
4 changes: 2 additions & 2 deletions crates/sessions/example_project/minimal.toml
BD0
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@

[upstream]
repo = "https://github.com/gominimal/pkgs"
branch = "main"
branch = "unstable"
# Reused from the workspace's own `minimal.toml` — guaranteed to
# resolve as long as that pin does.
locked_commit = "23193970569b93e5e7a980aa598c3342c386d33f"
locked_commit = "75884ceb9d227e7c97642ccdfff01fecbb8380a1"

[stack]
use = "shell"
Expand Down
10 changes: 10 additions & 0 deletions crates/sessions/src/core/loadout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,16 @@ impl Loadout {
new
}

/// Drop every lifecycle hook from this loadout, returning the
/// hookless loadout. Used to exclude session transition scripts
/// before composition while the feature is gated off for a release,
/// so a declared hook cannot participate in composition at all.
#[must_use]
pub fn without_lifecycle_hooks(mut self) -> Self {
self.lifecycle_hooks.clear();
self
}

/// Override the composer's default follow-symlinks behavior for
/// this loadout's patches.
#[must_use]
Expand Down
1 change: 0 additions & 1 deletion crates/sessions/src/daemon/composer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,6 @@ impl SessionComposer {
daemon_vars = vars.len(),
daemon_patches = patches.len(),
daemon_packages = packages.len(),
daemon_lifecycle_hooks = lifecycle_hooks.len(),
"compose: daemon-side contribution collected",
);
if vars.is_empty() && patches.is_empty() {
Expand Down
0