valise-sh 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/compiler.awk CHANGED
@@ -4,7 +4,7 @@
4
4
  # valise compile: pure POSIX awk static plan compiler.
5
5
  # Input: single preprocessed stream from sh wrapper.
6
6
  # Lines starting with SEED: space-separated seed module names
7
- # Lines starting with MOD: identity<TAB>requires<TAB>load_path
7
+ # Lines starting with MOD: identity<TAB>requires<TAB>load_path<TAB>platforms<TAB>side_effect<TAB>exports<TAB>optional
8
8
  # identity is module@meta_source when registry sources are qualified.
9
9
  # Lines starting with PRELUDE: sp_cap function names (space-separated)
10
10
  # Output: complete installed plan on stdout.
@@ -33,10 +33,14 @@ BEGIN {
33
33
  /^MOD:/ {
34
34
  line = substr($0, 5)
35
35
  n = split(line, fields, "\t")
36
- if (n >= 3) {
36
+ if (n >= 7) {
37
37
  name = fields[1]
38
38
  mod_req[name] = fields[2]
39
39
  mod_load[name] = fields[3]
40
+ mod_platforms[name] = fields[4]
41
+ mod_side_effect[name] = fields[5]
42
+ mod_exports[name] = fields[6]
43
+ mod_opt[name] = fields[7]
40
44
  display_name = name
41
45
  at = index(name, "@")
42
46
  if (at > 0) display_name = substr(name, 1, at - 1)
@@ -48,6 +52,19 @@ BEGIN {
48
52
  next
49
53
  }
50
54
 
55
+ /^EXCLUDED:/ {
56
+ # A module removed by the active build variant. It is not part of the
57
+ # closure, but it is named so a required dependency that points at it can
58
+ # be diagnosed precisely instead of as an unknown module. The record is
59
+ # "<identity>\t<name>"; the bare name is how a dependency refers to it.
60
+ line = substr($0, 10)
61
+ n = split(line, fields, "\t")
62
+ if (n >= 2) {
63
+ excluded_id[fields[1]] = fields[2]
64
+ }
65
+ next
66
+ }
67
+
51
68
  /^PRELUDE:/ {
52
69
  line = substr($0, 9)
53
70
  n = split(line, arr, " ")
@@ -88,6 +105,14 @@ END {
88
105
 
89
106
  if (name in discovered) continue
90
107
  if (!(name in mod_exists)) {
108
+ if (name in excluded_id) {
109
+ # The module was removed by the active build variant, but a
110
+ # surviving module still requires it. Name both modules: the
111
+ # generic "unknown module" diagnostic would point at the
112
+ # dependency and hide the variant that caused it.
113
+ print "ERROR: variant exclusion breaks a required dependency: " mod_display[requiring_of[name]] " requires " excluded_id[name] > "/dev/stderr"
114
+ exit 1
115
+ }
91
116
  print "ERROR: unknown module: " name > "/dev/stderr"
92
117
  exit 1
93
118
  }
@@ -109,6 +134,25 @@ END {
109
134
  queue[q_tail] = dep
110
135
  }
111
136
  }
137
+ # Remember which module asked for a dependency, so an exclusion
138
+ # that breaks it can name the requiring module.
139
+ if (!(dep in requiring_of)) requiring_of[dep] = name
140
+ }
141
+
142
+ # The preprocessor keeps only optional dependencies that exist for this
143
+ # shell and variant, so each of these is a module the plan must attempt.
144
+ n_opt = split(mod_opt[name], opts, " ")
145
+ for (ri = 1; ri <= n_opt; ri++) {
146
+ dep = opts[ri]
147
+ if (dep in discovered) continue
148
+ in_q = 0
149
+ for (qi = q_head; qi <= q_tail; qi++) {
150
+ if (queue[qi] == dep) { in_q = 1; break }
151
+ }
152
+ if (!in_q) {
153
+ q_tail++
154
+ queue[q_tail] = dep
155
+ }
112
156
  }
113
157
 
114
158
  # Re-sort remaining queue
@@ -141,6 +185,27 @@ END {
141
185
  rem_count = discovered_count
142
186
  ordered_count = 0
143
187
 
188
+ # The required closure is the roots and everything they require. A module
189
+ # reached only through an optional edge may be unavailable without failing
190
+ # the plan. Compute it before ordering so a stall can distinguish a cycle
191
+ # inside the required closure from an optional-only cycle.
192
+ for (i = 1; i <= seed_count; i++) {
193
+ required_stack[++required_top] = seeds[i]
194
+ }
195
+ while (required_top > 0) {
196
+ name = required_stack[required_top--]
197
+ if (name in required) continue
198
+ required[name] = 1
199
+ n_req = split(mod_req[name], reqs, " ")
200
+ for (rqi = 1; rqi <= n_req; rqi++) {
201
+ required_stack[++required_top] = reqs[rqi]
202
+ }
203
+ }
204
+
205
+ # Order by required and optional edges. When that stalls, retry on required
206
+ # edges alone, as the loader does, so an optional edge can never deadlock the
207
+ # graph. Only a stall on required edges is a cycle.
208
+ relaxed = 0
144
209
  while (rem_count > 0) {
145
210
  progress = 0
146
211
  next_rem_count = 0
@@ -149,13 +214,20 @@ END {
149
214
  all_loaded = 1
150
215
  n_req = split(mod_req[name], reqs, " ")
151
216
  for (rqi = 1; rqi <= n_req; rqi++) {
152
- dep = reqs[rqi]
153
- if (dep == "") continue
154
- if (!(dep in loaded)) {
217
+ if (!(reqs[rqi] in loaded)) {
155
218
  all_loaded = 0
156
219
  break
157
220
  }
158
221
  }
222
+ if (all_loaded && !relaxed) {
223
+ n_opt = split(mod_opt[name], opts, " ")
224
+ for (rqi = 1; rqi <= n_opt; rqi++) {
225
+ if (!(opts[rqi] in loaded)) {
226
+ all_loaded = 0
227
+ break
228
+ }
229
+ }
230
+ }
159
231
  if (all_loaded) {
160
232
  ordered_count++
161
233
  ordered[ordered_count] = name
@@ -167,18 +239,76 @@ END {
167
239
  }
168
240
  }
169
241
  if (!progress) {
170
- print "ERROR: cycle detected" > "/dev/stderr"
171
- exit 1
242
+ if (!relaxed) {
243
+ relaxed = 1
244
+ continue
245
+ }
246
+ # Every remaining module is in a cycle. The loader quarantines a
247
+ # cycle that lies wholly outside the required closure and still
248
+ # loads the rest, so only a cycle containing a required module is
249
+ # a compile error.
250
+ stalled_required = 0
251
+ for (ri = 1; ri <= rem_count; ri++) {
252
+ if (remaining[ri] in required) {
253
+ stalled_required = 1
254
+ break
255
+ }
256
+ }
257
+ if (stalled_required) {
258
+ print "ERROR: cycle detected" > "/dev/stderr"
259
+ exit 1
260
+ }
261
+ for (ri = 1; ri <= rem_count; ri++) {
262
+ print "warning: optional dependency cycle ignored: " remaining[ri] > "/dev/stderr"
263
+ }
264
+ rem_count = 0
265
+ break
172
266
  }
267
+ relaxed = 0
173
268
  rem_count = next_rem_count
174
269
  for (ri = 1; ri <= rem_count; ri++) {
175
270
  remaining[ri] = next_remaining[ri]
176
271
  }
177
272
  }
178
273
 
274
+ # Validate the ordered closure before rendering. Unsafe side effects are a
275
+ # compile-time error, and a bodyless module must still be a dependency-only
276
+ # meta module.
277
+ for (oi = 1; oi <= ordered_count; oi++) {
278
+ name = ordered[oi]
279
+ side_effect = mod_side_effect[name]
280
+ if (side_effect == "" || side_effect == "generated-file" || side_effect == "update-network" || side_effect == "install-time") {
281
+ print "ERROR: module " mod_display[name] " has unsafe side_effect for compile: " side_effect > "/dev/stderr"
282
+ exit 1
283
+ }
284
+ if (mod_load[name] == "" && mod_req[name] == "") {
285
+ print "ERROR: module " mod_display[name] " has no load path and no dependencies" > "/dev/stderr"
286
+ exit 1
287
+ }
288
+ if (mod_load[name] == "" && mod_exports[name] != "") {
289
+ print "ERROR: meta module " mod_display[name] " declares exports without a body" > "/dev/stderr"
290
+ exit 1
291
+ }
292
+ }
293
+
294
+ # Collect the plan's declared exports once for VALISE_RELOAD support.
295
+ reload_export_count = 0
296
+ for (oi = 1; oi <= ordered_count; oi++) {
297
+ name = ordered[oi]
298
+ n_exports = split(mod_exports[name], export_values, " ")
299
+ for (ei = 1; ei <= n_exports; ei++) {
300
+ export_name = export_values[ei]
301
+ if (export_name != "" && !(export_name in reload_export_seen)) {
302
+ reload_export_seen[export_name] = 1
303
+ reload_export_count++
304
+ reload_exports[reload_export_count] = export_name
305
+ }
306
+ }
307
+ }
308
+
179
309
  # --- Render plan ---
180
310
  printf "# shell-profile-installed-modules-v1\n"
181
- printf "# Generated by hack/compile-shell-profile-release.py; DO NOT EDIT.\n"
311
+ printf "# Generated by valise compile; DO NOT EDIT.\n"
182
312
  printf "# plan=%s\n", plan_name
183
313
  printf "# roots="
184
314
  for (i = 1; i <= seed_count; i++) {
@@ -192,25 +322,216 @@ END {
192
322
  printf "%s", mod_display[ordered[i]]
193
323
  }
194
324
  printf "\n\n"
325
+
195
326
  printf "%sstatus=0\n", prefix
327
+ printf "%sfailed=0\n", prefix
328
+ printf "%sfirst_status=0\n", prefix
329
+ printf "%sunavailable=\n", prefix
330
+ printf "%smode=${VALISE_MODE:-strict}\n", prefix
331
+ printf "case \"$%smode\" in\n", prefix
332
+ printf "strict | permissive) ;;\n"
333
+ printf "*)\n"
334
+ printf " printf '%%s\\n' \"valise: invalid mode: $%smode\" >&2\n", prefix
335
+ printf " unset %sstatus %sfailed %sfirst_status %sunavailable %smode\n", prefix, prefix, prefix, prefix, prefix
336
+ printf " return 1\n"
337
+ printf " ;;\n"
338
+ printf "esac\n"
339
+
340
+ printf "%splatform=$(command uname -s 2>/dev/null || printf unknown)\n", prefix
341
+ printf "case \"$%splatform\" in\n", prefix
342
+ printf "Darwin) %splatform=darwin ;;\n", prefix
343
+ printf "Linux) %splatform=linux ;;\n", prefix
344
+ printf "FreeBSD) %splatform=freebsd ;;\n", prefix
345
+ printf "OpenBSD) %splatform=openbsd ;;\n", prefix
346
+ printf "NetBSD) %splatform=netbsd ;;\n", prefix
347
+ printf "*BSD) %splatform=bsd ;;\n", prefix
348
+ printf "*) %splatform=unknown ;;\n", prefix
349
+ printf "esac\n"
350
+
351
+ if (reload_export_count > 0) {
352
+ printf "if [ \"${VALISE_RELOAD:-}\" = 1 ]; then\n"
353
+ printf " for %sexport in", prefix
354
+ for (i = 1; i <= reload_export_count; i++) {
355
+ printf " %s", reload_exports[i]
356
+ }
357
+ printf "; do\n"
358
+ printf " unset -f \"$%sexport\" 2>/dev/null || :\n", prefix
359
+ printf " done\n"
360
+ printf " unset %sexport\n", prefix
361
+ printf "fi\n"
362
+ }
363
+
196
364
  if (prelude_fn_count > 0) {
197
365
  printf ". \"${VALISE_MODULE_ROOT%%/}/%s\" || %sstatus=$?\n", prelude_dest, prefix
366
+ printf "if [ \"$%sstatus\" -ne 0 ]; then\n", prefix
367
+ printf " printf 'valise: capability prelude failed with status %%s\\n' \"$%sstatus\" >&2\n", prefix
368
+ printf " %sfailed=1\n", prefix
369
+ printf " %sfirst_status=$%sstatus\n", prefix, prefix
370
+ printf "fi\n"
198
371
  }
372
+
373
+ printf "if [ \"$%sstatus\" -eq 0 ]; then\n", prefix
374
+ printf " for %smodule in", prefix
199
375
  for (i = 1; i <= ordered_count; i++) {
200
- name = ordered[i]
201
- printf "if [ \"${%sstatus}\" -eq 0 ]; then\n", prefix
202
- printf " . \"${VALISE_MODULE_ROOT%%/}/%s\"\n", mod_load[name]
203
- printf " %sstatus=$?\n", prefix
204
- printf "fi\n"
376
+ printf " %s", ordered[i]
205
377
  }
206
- if (prelude_fn_count > 0) {
207
- printf "unset -f"
208
- for (i = 1; i <= prelude_fn_count; i++) {
209
- printf " %s", prelude_fns[i]
378
+ printf "; do\n"
379
+ printf " %srequired=1\n", prefix
380
+ printf " case \"$%smodule\" in\n", prefix
381
+
382
+ for (oi = 1; oi <= ordered_count; oi++) {
383
+ name = ordered[oi]
384
+ display = mod_display[name]
385
+ printf " %s)\n", name
386
+ platform_any = 0
387
+ n_platforms = split(mod_platforms[name], platform_values, " ")
388
+ for (pi = 1; pi <= n_platforms; pi++) {
389
+ if (platform_values[pi] == "any") platform_any = 1
210
390
  }
211
- printf " 2>/dev/null || :\n"
391
+ export_count = split(mod_exports[name], module_exports, " ")
392
+ body_indent = platform_any == 0 ? " " : " "
393
+
394
+ # An optional-only module is expected to be unavailable on some hosts,
395
+ # so it fails quietly; the loop below does not treat it as a plan failure.
396
+ if (!(name in required)) printf " %srequired=0\n", prefix
397
+
398
+ # A module whose required dependency is unavailable must not run: its
399
+ # body would call functions that were never defined.
400
+ n_req = split(mod_req[name], reqs, " ")
401
+ if (n_req > 0) {
402
+ printf " %sblocked_by=\n", prefix
403
+ printf " case \" $%sunavailable \" in", prefix
404
+ for (rqi = 1; rqi <= n_req; rqi++) {
405
+ printf " *\" %s \"*) %sblocked_by=%s ;;", reqs[rqi], prefix, mod_display[reqs[rqi]]
406
+ }
407
+ printf " esac\n"
408
+ printf " if [ -n \"$%sblocked_by\" ]; then\n", prefix
409
+ if (name in required) {
410
+ printf " printf 'valise: module %%s skipped: required dependency unavailable: %%s\\n' '%s' \"$%sblocked_by\" >&2\n", display, prefix
411
+ }
412
+ printf " %sstatus=1\n", prefix
413
+ printf " else\n"
414
+ # A meta module on any platform has no statement for this branch.
415
+ if (mod_load[name] == "" && platform_any == 1) printf " :\n"
416
+ }
417
+
418
+ if (platform_any == 0) {
419
+ printf " %splatform_match=0\n", prefix
420
+ for (pi = 1; pi <= n_platforms; pi++) {
421
+ platform = platform_values[pi]
422
+ if (platform == "bsd") {
423
+ printf " case \"$%splatform\" in bsd | freebsd | openbsd | netbsd) %splatform_match=1 ;; esac\n", prefix, prefix
424
+ } else if (platform != "any") {
425
+ printf " [ \"$%splatform\" = %s ] && %splatform_match=1\n", prefix, platform, prefix
426
+ }
427
+ }
428
+ printf " if [ \"$%splatform_match\" -ne 1 ]; then\n", prefix
429
+ if (name in required) {
430
+ printf " printf 'valise: module %%s skipped: unsupported platform: %%s\\n' '%s' \"$%splatform\" >&2\n", display, prefix
431
+ }
432
+ printf " %sstatus=1\n", prefix
433
+ if (mod_load[name] != "") printf " else\n"
434
+ }
435
+ if (mod_load[name] != "") {
436
+ if (export_count == 0) {
437
+ printf "%s. \"${VALISE_MODULE_ROOT%%/}/%s\"\n", body_indent, mod_load[name]
438
+ printf "%s%sstatus=$?\n", body_indent, prefix
439
+ printf "%sif [ \"$%sstatus\" -ne 0 ]; then\n", body_indent, prefix
440
+ printf "%s printf 'valise: module %%s failed with status %%s\\n' '%s' \"$%sstatus\" >&2\n", body_indent, display, prefix
441
+ printf "%sfi\n", body_indent
442
+ } else {
443
+ printf "%s%swrapper_exports=\n", body_indent, prefix
444
+ printf "%s%scleanup_exports=\n", body_indent, prefix
445
+ printf "%sfor %sexport in", body_indent, prefix
446
+ for (ei = 1; ei <= export_count; ei++) {
447
+ printf " %s", module_exports[ei]
448
+ }
449
+ printf "; do\n"
450
+ printf "%s %sconflict=0\n", body_indent, prefix
451
+ printf "%s if command alias \"$%sexport\" >/dev/null 2>&1; then\n", body_indent, prefix
452
+ printf "%s %sconflict=1\n", body_indent, prefix
453
+ printf "%s fi\n", body_indent
454
+ printf "%s if %slookup=$(command -v \"$%sexport\" 2>/dev/null); then\n", body_indent, prefix, prefix
455
+ printf "%s case \"$%slookup\" in\n", body_indent, prefix
456
+ printf "%s alias\\ *) %sconflict=1 ;;\n", body_indent, prefix
457
+ printf "%s */*) %swrapper_exports=\"${%swrapper_exports:+$%swrapper_exports }$%sexport\" ;;\n", body_indent, prefix, prefix, prefix, prefix, prefix
458
+ printf "%s *) %sconflict=1 ;;\n", body_indent, prefix
459
+ printf "%s esac\n", body_indent
460
+ printf "%s else\n", body_indent
461
+ printf "%s %slookup=\n", body_indent, prefix
462
+ printf "%s fi\n", body_indent
463
+ printf "%s if [ \"$%sconflict\" -ne 0 ]; then\n", body_indent, prefix
464
+ printf "%s printf 'valise: %%s: declared export already exists before load: %%s\\n' '%s' \"$%sexport\" >&2\n", body_indent, display, prefix
465
+ printf "%s %sstatus=1\n", body_indent, prefix
466
+ printf "%s else\n", body_indent
467
+ printf "%s %scleanup_exports=\"${%scleanup_exports:+$%scleanup_exports }$%sexport\"\n", body_indent, prefix, prefix, prefix, prefix, prefix
468
+ printf "%s fi\n", body_indent
469
+ printf "%sdone\n", body_indent
470
+ printf "%sif [ \"$%sstatus\" -eq 0 ]; then\n", body_indent, prefix
471
+ printf "%s . \"${VALISE_MODULE_ROOT%%/}/%s\"\n", body_indent, mod_load[name]
472
+ printf "%s %sstatus=$?\n", body_indent, prefix
473
+ printf "%sfi\n", body_indent
474
+ printf "%sif [ \"$%sstatus\" -eq 0 ]; then\n", body_indent, prefix
475
+ printf "%s for %sexport in", body_indent, prefix
476
+ for (ei = 1; ei <= export_count; ei++) {
477
+ printf " %s", module_exports[ei]
478
+ }
479
+ printf "; do\n"
480
+ printf "%s if ! %slookup=$(command -v \"$%sexport\" 2>/dev/null); then\n", body_indent, prefix, prefix
481
+ printf "%s printf 'valise: %%s: declared export missing: %%s\\n' '%s' \"$%sexport\" >&2\n", body_indent, display, prefix
482
+ printf "%s %sstatus=1\n", body_indent, prefix
483
+ printf "%s else\n", body_indent
484
+ printf "%s case \" $%swrapper_exports \" in\n", body_indent, prefix
485
+ printf "%s *\" $%sexport \"*)\n", body_indent, prefix
486
+ printf "%s case \"$%slookup\" in\n", body_indent, prefix
487
+ printf "%s */*)\n", body_indent
488
+ printf "%s printf 'valise: %%s: declared export was not provided by module: %%s\\n' '%s' \"$%sexport\" >&2\n", body_indent, display, prefix
489
+ printf "%s %sstatus=1\n", body_indent, prefix
490
+ printf "%s ;;\n", body_indent
491
+ printf "%s esac\n", body_indent
492
+ printf "%s ;;\n", body_indent
493
+ printf "%s esac\n", body_indent
494
+ printf "%s fi\n", body_indent
495
+ printf "%s done\n", body_indent
496
+ printf "%sfi\n", body_indent
497
+ printf "%sif [ \"$%sstatus\" -ne 0 ]; then\n", body_indent, prefix
498
+ printf "%s printf 'valise: module %%s failed with status %%s\\n' '%s' \"$%sstatus\" >&2\n", body_indent, display, prefix
499
+ printf "%s for %sexport in $%scleanup_exports; do\n", body_indent, prefix, prefix
500
+ printf "%s unset -f \"$%sexport\" 2>/dev/null || :\n", body_indent, prefix
501
+ printf "%s done\n", body_indent
502
+ printf "%sfi\n", body_indent
503
+ }
504
+ }
505
+ if (platform_any == 0) {
506
+ printf " fi\n"
507
+ }
508
+ if (n_req > 0) {
509
+ printf " fi\n"
510
+ }
511
+ printf " ;;\n"
212
512
  }
213
- printf "%sreturn=$%sstatus\n", prefix, prefix
214
- printf "unset %sstatus\n", prefix
513
+
514
+ printf " esac\n"
515
+ # Strict mode stops at the first failure in the required closure. Anything
516
+ # else is recorded so its dependents are blocked, and the plan continues.
517
+ printf " if [ \"$%sstatus\" -ne 0 ]; then\n", prefix
518
+ printf " %sunavailable=\"$%sunavailable $%smodule\"\n", prefix, prefix, prefix
519
+ printf " if [ \"$%smode\" = strict ] && [ \"$%srequired\" -eq 1 ]; then\n", prefix, prefix
520
+ printf " %sfailed=1\n", prefix
521
+ printf " %sfirst_status=$%sstatus\n", prefix, prefix
522
+ printf " break\n"
523
+ printf " fi\n"
524
+ printf " %sstatus=0\n", prefix
525
+ printf " fi\n"
526
+ printf " done\n"
527
+ printf "fi\n"
528
+
529
+ printf "if [ \"$%sfailed\" -ne 0 ]; then\n", prefix
530
+ printf " %sreturn=$%sfirst_status\n", prefix, prefix
531
+ printf "else\n"
532
+ printf " %sreturn=0\n", prefix
533
+ printf "fi\n"
534
+ printf "unset %sstatus %sfailed %sfirst_status %sunavailable %smode %splatform\n", prefix, prefix, prefix, prefix, prefix, prefix
535
+ printf "unset %smodule %srequired %sblocked_by %splatform_match %sexport %slookup %sconflict %swrapper_exports %scleanup_exports\n", prefix, prefix, prefix, prefix, prefix, prefix, prefix, prefix, prefix
215
536
  printf "eval \"unset %sreturn; return $%sreturn\"\n", prefix, prefix
216
537
  }
@@ -728,8 +728,8 @@ function sp_fish_loader_validate_provider_conflicts --description 'Reject confli
728
728
  end
729
729
  set -l candidate_load_line (string match 'load=*' -- $candidate_parsed)
730
730
  set -l candidate_load (string replace 'load=' '' -- $candidate_load_line)
731
- # Generic .sh/load_hook providers are graph metadata only for Fish;
732
- # they have no native body whose ownership can be compared.
731
+ # Generic .sh providers are graph metadata only for Fish; they
732
+ # have no native body whose ownership can be compared.
733
733
  if not string match -q '*.fish' -- "$primary_load"; or not string match -q '*.fish' -- "$candidate_load"
734
734
  continue
735
735
  end
@@ -931,6 +931,8 @@ function _sp_fish_loader_load_ordered_impl --description 'Load modules from seed
931
931
  set -l meta_provider_modules
932
932
  set -l meta_provider_paths
933
933
  set -l optional_modules
934
+ set -l platform_skipped_modules
935
+ set -l requires_edges
934
936
  set -l shell_skipped_modules
935
937
  set -l failed_modules
936
938
  set -l runtime_failed_modules
@@ -1029,15 +1031,7 @@ function _sp_fish_loader_load_ordered_impl --description 'Load modules from seed
1029
1031
  end
1030
1032
  end
1031
1033
  if test $platform_match -ne 1
1032
- if builtin contains -- $current $optional_modules
1033
- sp_fish_loader_error "warning: optional module unsupported on platform: $current"
1034
- set -a failed_modules $current
1035
- continue
1036
- end
1037
- if test "$mode" = strict
1038
- sp_fish_loader_error "unsupported platform for $current: $current_platform"
1039
- return 1
1040
- end
1034
+ set -a platform_skipped_modules $current
1041
1035
  set -a failed_modules $current
1042
1036
  continue
1043
1037
  end
@@ -1063,12 +1057,20 @@ function _sp_fish_loader_load_ordered_impl --description 'Load modules from seed
1063
1057
  continue
1064
1058
  end
1065
1059
 
1066
- # A Fish-advertised module must provide a native Fish body. Generic
1067
- # load_hook records and POSIX .sh bodies are not executable Fish
1068
- # providers and must not satisfy required Fish dependents.
1060
+ # A Fish-advertised module must provide a native Fish body, or be a
1061
+ # dependency-only meta record. Generic POSIX bodies are not executable
1062
+ # Fish providers and must not satisfy required Fish dependents.
1069
1063
  set -l load_line (string match 'load=*' -- $parsed)
1070
1064
  set -l load_val (string replace 'load=' '' -- $load_line)
1071
- if test -z "$load_val"; or not string match -q '*.fish' -- $load_val
1065
+ set -l meta_requires_line (string match 'requires=*' -- $parsed)
1066
+ set -l meta_requires_val (string replace 'requires=' '' -- $meta_requires_line)
1067
+ set -l is_meta 0
1068
+ if test -z "$load_val"; and test -n "$meta_requires_val"
1069
+ set is_meta 1
1070
+ end
1071
+ if test $is_meta -eq 0; and begin
1072
+ test -z "$load_val"; or not string match -q '*.fish' -- $load_val
1073
+ end
1072
1074
  if builtin contains -- $current $optional_modules; and not builtin contains -- $current $explicit_seeds
1073
1075
  sp_fish_loader_error "warning: optional Fish module has no native .fish load path: $current"
1074
1076
  set -a failed_modules $current
@@ -1106,6 +1108,7 @@ function _sp_fish_loader_load_ordered_impl --description 'Load modules from seed
1106
1108
  # Enqueue requires dependencies
1107
1109
  set -l req_line (string match 'requires=*' -- $parsed)
1108
1110
  set -l req_val (string replace 'requires=' '' -- $req_line)
1111
+ set -a requires_edges "$current|$req_val"
1109
1112
  for dep in (string split ' ' -- $req_val)
1110
1113
  if test -z "$dep"
1111
1114
  continue
@@ -1134,6 +1137,42 @@ function _sp_fish_loader_load_ordered_impl --description 'Load modules from seed
1134
1137
  end
1135
1138
  end
1136
1139
 
1140
+ # A platform skip is expected for an optional-only module and stays
1141
+ # silent. Re-walk the required edges to a fixpoint first, because discovery
1142
+ # can mark a module optional before a later required edge promotes it; a
1143
+ # required-closure platform skip is a strict failure and must be named.
1144
+ set -l required_modules $seeds
1145
+ set -l closure_progress 1
1146
+ while test $closure_progress -eq 1
1147
+ set closure_progress 0
1148
+ for edge in $requires_edges
1149
+ set -l edge_parts (string split '|' -- $edge)
1150
+ set -l edge_from $edge_parts[1]
1151
+ set -l edge_deps (string split ' ' -- $edge_parts[2])
1152
+ if not builtin contains -- $edge_from $required_modules
1153
+ continue
1154
+ end
1155
+ for edge_dep in $edge_deps
1156
+ if test -z "$edge_dep"; or builtin contains -- $edge_dep $required_modules
1157
+ continue
1158
+ end
1159
+ set -a required_modules $edge_dep
1160
+ set closure_progress 1
1161
+ end
1162
+ end
1163
+ end
1164
+ set -l report_platform (sp_fish_loader_platform_name)
1165
+ test -n "$report_platform"; or set report_platform unknown
1166
+ for skipped_module in $platform_skipped_modules
1167
+ if not builtin contains -- $skipped_module $required_modules
1168
+ continue
1169
+ end
1170
+ sp_fish_loader_error "unsupported platform for $skipped_module: $report_platform"
1171
+ if test "$mode" = strict
1172
+ return 1
1173
+ end
1174
+ end
1175
+
1137
1176
  # Phase 2: Kahn's algorithm for deterministic topological sort
1138
1177
  set -l remaining $discovered
1139
1178
  set -l ordered
@@ -1343,6 +1382,33 @@ function _sp_fish_loader_load_ordered_impl --description 'Load modules from seed
1343
1382
  end
1344
1383
 
1345
1384
  if test -z "$load_path"
1385
+ # A meta module has no body: its dependencies have already loaded,
1386
+ # and it must not claim exports it cannot define.
1387
+ if test (builtin count $mod_reqs) -gt 0
1388
+ set -l meta_export_count 0
1389
+ for meta_export in $mod_exports
1390
+ if test -n "$meta_export"
1391
+ set meta_export_count (builtin math $meta_export_count + 1)
1392
+ end
1393
+ end
1394
+ if test $meta_export_count -gt 0
1395
+ if builtin contains -- $mod $optional_modules
1396
+ sp_fish_loader_error "warning: optional meta module declares exports without a body: $mod"
1397
+ set -a failed_modules $mod
1398
+ set -a runtime_failed_modules $mod
1399
+ continue
1400
+ end
1401
+ if test "$mode" = strict
1402
+ sp_fish_loader_error "meta module declares exports without a body: $mod"
1403
+ return 1
1404
+ end
1405
+ set -a runtime_failed_modules $mod
1406
+ sp_fish_loader_error "warning: meta module declares exports without a body: $mod"
1407
+ continue
1408
+ end
1409
+ set -g _sp_fish_loaded_modules $_sp_fish_loaded_modules $mod
1410
+ continue
1411
+ end
1346
1412
  if builtin contains -- $mod $optional_modules
1347
1413
  sp_fish_loader_error "warning: optional module has no load path: $mod"
1348
1414
  set -a failed_modules $mod
@@ -1469,15 +1535,24 @@ function _sp_fish_loader_load_ordered_impl --description 'Load modules from seed
1469
1535
  set -l caller_function_copy_failed 0
1470
1536
  builtin functions --copy "$caller_function_name" "$caller_function_snapshot"; or set caller_function_copy_failed 1
1471
1537
  set -l caller_function_snapshot_source (builtin functions --details "$caller_function_snapshot")
1538
+ set -l caller_function_copy_matches 0
1539
+ if _sp_fish_loader_function_copy_matches "$caller_function_name" "$caller_function_snapshot"; or _sp_fish_loader_is_trusted_system_function_source "$caller_function_source" "$caller_function_name"
1540
+ set caller_function_copy_matches 1
1541
+ end
1472
1542
  set -l caller_function_source_authenticated 0
1473
1543
  if builtin test "$caller_function_snapshot_source" = "$loader_source_file"
1474
1544
  set caller_function_source_authenticated 1
1545
+ else if builtin test "$caller_function_snapshot_source" = "$caller_function_source"
1546
+ # Fish keeps the copy's original source; a copy that agrees
1547
+ # with the live function's source is a faithful custody copy.
1548
+ set caller_function_source_authenticated 1
1475
1549
  else if _sp_fish_loader_is_trusted_system_function_source "$caller_function_source" "$caller_function_name"
1476
1550
  set caller_function_source_authenticated 1
1477
- end
1478
- set -l caller_function_copy_matches 0
1479
- if _sp_fish_loader_function_copy_matches "$caller_function_name" "$caller_function_snapshot"; or _sp_fish_loader_is_trusted_system_function_source "$caller_function_source" "$caller_function_name"
1480
- set caller_function_copy_matches 1
1551
+ else if builtin test $caller_function_copy_matches -eq 1
1552
+ # Fish 3 reports a copied function's source as ``stdin``. The
1553
+ # copy was made by Fish's own primitive and matches the live
1554
+ # definition; later restores re-verify that definition.
1555
+ set caller_function_source_authenticated 1
1481
1556
  end
1482
1557
  if builtin functions -q "$caller_function_snapshot"; and builtin test $caller_function_copy_failed -eq 0; and builtin test $caller_function_source_authenticated -eq 1; and builtin test $caller_function_copy_matches -eq 1
1483
1558
  # The source and complete definition were authenticated.