Skip to content

Commit 3237e44

Browse files
exodistclaude
andcommitted
Keep @inc hooks across the dev-lib re-exec
A dev-lib flag makes the script re-exec itself, carrying @inc across in T2_HARNESS_INCLUDES. That variable can only hold paths, so a hook was serialized as "Foo=HASH(0x...)" and came back as a nonexistent directory, while the real hook PERL5OPT had already reinstalled in the new process was thrown away by the script and inject_includes() both replacing @inc outright. Under Carmel that is fatal: its modules are reachable through nothing but its hook, so the re-exec'd yath could not load its own plugins. Serialize only paths, and keep any hook the new process already has. The script needs its own copy of the logic because it sets @inc up before App::Yath::Script can be loaded, which also means an installed yath script must be replaced for the fix to take effect. Written with AI assistance. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 1dbf186 commit 3237e44

5 files changed

Lines changed: 66 additions & 3 deletions

File tree

Changes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
- Replace AI_AND_LLM_POLICY.txt with a rewritten AI_AND_LLM_POLICY.md.
44
- Keep internal docs, agent tooling and TEMPLATE.pod out of the tarball.
5+
- Keep @INC hooks, such as Carmel's, working when the script re-execs itself for dev-libs.
56

67
2.000016 2026-04-28 22:01:21-07:00 America/Los_Angeles
78

lib/App/Yath/Script.pm

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,10 @@ sub do_runtime { $MOD->do_runtime(@_) }
8181

8282
sub do_exec {
8383
my ($argv) = @_;
84-
$ENV{T2_HARNESS_INCLUDES} = join ';' => @INC;
84+
# Hooks (coderef, arrayref, blessed object) cannot be serialized. Anything
85+
# that installed one via PERL5OPT installs it again in the new process, so
86+
# only the paths need to survive the exec.
87+
$ENV{T2_HARNESS_INCLUDES} = join ';' => grep { !ref $_ } @INC;
8588
exec($^X, $SCRIPT, @$argv);
8689
}
8790

@@ -279,7 +282,10 @@ sub load_latest_yath_module {
279282

280283
sub inject_includes {
281284
return unless $ENV{T2_HARNESS_INCLUDES};
282-
@INC = split /;/, $ENV{T2_HARNESS_INCLUDES};
285+
# Keep any hooks this process already has. PERL5OPT runs before us, so a
286+
# tool like Carmel has already reinstalled the hook that makes its modules
287+
# findable, and replacing @INC outright would throw it away.
288+
@INC = (grep({ ref $_ } @INC), split(/;/, $ENV{T2_HARNESS_INCLUDES}));
283289
}
284290

285291
# Scan ./lib/App/Yath/Script for V#.pm modules and return the highest
@@ -527,6 +533,8 @@ Returns the name of the currently loaded C<App::Yath::Script::V{X}> module.
527533
528534
Re-executes the current script with the given arguments. Sets the
529535
C<T2_HARNESS_INCLUDES> environment variable to preserve the current C<@INC>.
536+
Only paths are preserved; C<@INC> hooks cannot cross an C<exec> and are
537+
reinstalled in the new process by whatever added them.
530538
531539
=item $clean_path = clean_path($path)
532540

scripts/yath

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ our $VERSION = '2.000017';
77
BEGIN {
88
return if $^C;
99

10-
@INC = split /;/, $ENV{T2_HARNESS_INCLUDES} if $ENV{T2_HARNESS_INCLUDES};
10+
# Same as App::Yath::Script::inject_includes(), which cannot run yet: any
11+
# @INC hook in this process was installed by PERL5OPT and must be kept.
12+
@INC = (grep({ ref $_ } @INC), split(/;/, $ENV{T2_HARNESS_INCLUDES}))
13+
if $ENV{T2_HARNESS_INCLUDES};
1114

1215
require App::Yath::Script;
1316

t/acceptance/yath.t

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,4 +205,48 @@ subtest 'CLI V# finds uppercase V rc filename' => sub {
205205
is($exit, 0, 'exit code is 0');
206206
};
207207

208+
subtest 'an @INC hook survives the dev-lib re-exec' => sub {
209+
my $tdir = tempdir(CLEANUP => 1);
210+
211+
# A dev-lib flag in the rc file makes the script re-exec itself. PERL5OPT
212+
# reinstalls the hook in the new process, so it must still be in @INC once
213+
# the script has finished setting @INC up. Carmel works exactly this way,
214+
# and its modules are reachable through nothing else.
215+
my $rcfile = File::Spec->catfile($tdir, '.yath.v0.rc');
216+
open(my $rcfh, '>', $rcfile) or die "Cannot write $rcfile: $!";
217+
print $rcfh "-D\n";
218+
close($rcfh) or die "Cannot close $rcfile: $!";
219+
220+
my $probe = File::Spec->catfile($tdir, 'HookProbe.pm');
221+
open(my $pfh, '>', $probe) or die "Cannot write $probe: $!";
222+
print $pfh <<'EOT';
223+
package HookProbe;
224+
use strict;
225+
use warnings;
226+
227+
# Fully qualified on purpose: perl compiles a bare `sub INC` into main::
228+
# no matter which package is current, so the method would never be found.
229+
sub HookProbe::INC { return }
230+
231+
unshift @INC, bless({}, __PACKAGE__);
232+
233+
END {
234+
print "HOOKS: " . scalar(grep { ref $_ } @INC) . "\n";
235+
print "CORPSES: " . scalar(grep { !ref($_) && m/HookProbe=HASH\(/ } @INC) . "\n";
236+
}
237+
238+
1;
239+
EOT
240+
close($pfh) or die "Cannot close $probe: $!";
241+
242+
local $ENV{PERL5OPT} = "-I$tdir -MHookProbe";
243+
my ($output, $exit) = run_yath_in($tdir, 'hook_arg');
244+
245+
like($output, qr/^RUNTIME: hook_arg$/m, 'runtime arg processed');
246+
like($output, qr/^HOOKS: 1$/m, 'hook is still in @INC after the re-exec');
247+
like($output, qr/^CORPSES: 0$/m, 'no stringified hook was put into @INC');
248+
249+
is($exit, 0, 'exit code is 0');
250+
};
251+
208252
done_testing;

t/unit/Script.t

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,13 @@ subtest 'inject_includes' => sub {
8686
App::Yath::Script::inject_includes();
8787
is(\@INC, ['/fake/path1', '/fake/path2'], 'replaces @INC from env var');
8888

89+
# A hook installed by PERL5OPT must survive. Only paths come from the env
90+
# var; it cannot carry a ref.
91+
my $hook = sub { return };
92+
@INC = ($hook, '/whatever');
93+
App::Yath::Script::inject_includes();
94+
is(\@INC, [exact_ref($hook), '/fake/path1', '/fake/path2'], 'keeps @INC hooks, replaces paths');
95+
8996
# Restore
9097
@INC = @orig_inc;
9198
};

0 commit comments

Comments
 (0)