@sous-io/sous 0.2.29 → 0.2.31
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/README.md +12 -0
- package/docs/markdown/README.md +52 -0
- package/docs/markdown/_sidebar.md +2 -0
- package/docs/markdown/commands.md +31 -6
- package/docs/markdown/repositories-authoring.md +7 -3
- package/docs/markdown/repositories-consuming.md +26 -12
- package/docs/markdown/repositories-file-formats.md +35 -5
- package/docs/markdown/repositories-variables.md +4 -1
- package/package.json +2 -1
- package/recipes/core/sous-skills/skills/about-sous-repos/SKILL.tpl.md +7 -4
- package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
- package/src/base-command.ts +75 -2
- package/src/commands/lock/rebuild.ts +5 -4
- package/src/commands/recipe/show.ts +163 -64
- package/src/commands/update.ts +178 -0
- package/src/lib/project-install.d.mts +4 -0
- package/src/lib/project-install.mjs +46 -8
- package/src/lib/repos/catalog.ts +43 -55
- package/src/lib/repos/declarations.ts +177 -0
- package/src/lib/repos/formats/common.ts +14 -0
- package/src/lib/repos/formats/index-file.ts +40 -1
- package/src/lib/repos/formats/lockfile.ts +5 -3
- package/src/lib/repos/formats/recipe-manifest.ts +6 -204
- package/src/lib/repos/formats/variable-definition.ts +268 -0
- package/src/lib/repos/providers/http.ts +37 -16
- package/src/lib/repos/release/index-builder.ts +168 -32
- package/src/lib/repos/resolver.ts +53 -12
- package/src/lib/repos/subscription-service.ts +93 -11
- package/src/lib/repos/update-plan.ts +3 -2
- package/src/lib/self-update/index.ts +14 -0
- package/src/lib/self-update/installs.ts +412 -0
- package/src/lib/self-update/managers.ts +126 -0
- package/src/lib/self-update/plan.ts +315 -0
- package/src/lib/self-update/registry.ts +99 -0
- package/src/lib/self-update/run.ts +506 -0
- package/src/lib/self-update/versions.ts +243 -0
- package/src/lib/vars/question-plan.ts +44 -21
|
@@ -26,6 +26,13 @@
|
|
|
26
26
|
* its ranges long afterwards. Those dependencies are resolved exactly once: when
|
|
27
27
|
* the version is first recorded, against the repository as it stands then (or,
|
|
28
28
|
* for a version rebuilt from its tag, as it stood at that tag), and never again.
|
|
29
|
+
*
|
|
30
|
+
* A version recorded here is also DESCRIBED, so a consumer can say what
|
|
31
|
+
* subscribing to it brings in and asks before anything is fetched: each
|
|
32
|
+
* dependency carries the manifest entry that declared it and whether it is a
|
|
33
|
+
* co-subscription or a build dependency, and the version carries the variable
|
|
34
|
+
* definitions its manifest publishes. Those fields are frozen with the rest of
|
|
35
|
+
* the entry; a version published before they existed keeps its entry as it is.
|
|
29
36
|
*/
|
|
30
37
|
|
|
31
38
|
import fs from "node:fs";
|
|
@@ -60,6 +67,14 @@ import {
|
|
|
60
67
|
type ValidatedRecipe,
|
|
61
68
|
type ValidationProblem,
|
|
62
69
|
} from "./validate.js";
|
|
70
|
+
import {
|
|
71
|
+
describeDependencyKind,
|
|
72
|
+
foldDeclaration,
|
|
73
|
+
type CoveringEntry,
|
|
74
|
+
type DependencyDeclaration,
|
|
75
|
+
type GatheredDeclaration,
|
|
76
|
+
} from "../declarations.js";
|
|
77
|
+
import type { DependencyKind } from "../formats/common.js";
|
|
63
78
|
import { isNamedReading, namespaceOfKey, parseRef, refKey } from "../../refs/parse.js";
|
|
64
79
|
import { RefSource } from "../../refs/scopes.js";
|
|
65
80
|
import type { SettledDependency } from "./settle.js";
|
|
@@ -204,7 +219,7 @@ export async function buildIndex(
|
|
|
204
219
|
tag: tag.tag,
|
|
205
220
|
prerelease: semver.prerelease(tag.version) !== null,
|
|
206
221
|
...(await releasedAtOf(rootDir, tag.tag, run)),
|
|
207
|
-
...
|
|
222
|
+
...rebuilt.description,
|
|
208
223
|
};
|
|
209
224
|
}
|
|
210
225
|
|
|
@@ -265,6 +280,9 @@ export async function buildIndex(
|
|
|
265
280
|
...checkRecordedDependencies(key, version, published.dependencies, declared)
|
|
266
281
|
);
|
|
267
282
|
}
|
|
283
|
+
if (taggedHash === workingHash) {
|
|
284
|
+
problems.push(...checkRecordedLists(key, version, published, recipe));
|
|
285
|
+
}
|
|
268
286
|
}
|
|
269
287
|
// A tagged version the index did not record was rebuilt from its tag above.
|
|
270
288
|
|
|
@@ -294,13 +312,12 @@ export async function buildIndex(
|
|
|
294
312
|
// follows on the very commit that carries this index.
|
|
295
313
|
const published = existingVersions[version];
|
|
296
314
|
if (options.publishing?.[key] === version || published !== undefined) {
|
|
297
|
-
const dependencies = resolveIndexDependencies(declared, current);
|
|
298
315
|
versions[version] = {
|
|
299
316
|
hash: workingHash,
|
|
300
317
|
tag: tagName,
|
|
301
318
|
prerelease: semver.prerelease(version) !== null,
|
|
302
319
|
releasedAt: published?.releasedAt ?? now.toISOString(),
|
|
303
|
-
...(
|
|
320
|
+
...describeVersion(recipe, declared, current),
|
|
304
321
|
};
|
|
305
322
|
}
|
|
306
323
|
|
|
@@ -435,10 +452,10 @@ export function describeIndexDrift(
|
|
|
435
452
|
* `DeclaredDependencies`. A whole-namespace declaration is expanded into one
|
|
436
453
|
* entry per recipe the namespace holds.
|
|
437
454
|
*/
|
|
438
|
-
type
|
|
455
|
+
type DeclaredLocation =
|
|
439
456
|
| {
|
|
440
457
|
/** A recipe in another repository, named by its location. */
|
|
441
|
-
|
|
458
|
+
where: "remote";
|
|
442
459
|
/** The canonical identity of the repository publishing it. */
|
|
443
460
|
repo: string;
|
|
444
461
|
/** The range the manifest declared, `*` when it declared none. */
|
|
@@ -446,7 +463,7 @@ type DeclaredDependency =
|
|
|
446
463
|
}
|
|
447
464
|
| {
|
|
448
465
|
/** A recipe in this same repository. */
|
|
449
|
-
|
|
466
|
+
where: "sibling";
|
|
450
467
|
/** The range the manifest declared, when it declared one. */
|
|
451
468
|
range?: string;
|
|
452
469
|
/**
|
|
@@ -458,6 +475,9 @@ type DeclaredDependency =
|
|
|
458
475
|
named: boolean;
|
|
459
476
|
};
|
|
460
477
|
|
|
478
|
+
/** One declared dependency: where it lives, and the manifest entry bringing it in. */
|
|
479
|
+
type DeclaredDependency = DeclaredLocation & DependencyDeclaration;
|
|
480
|
+
|
|
461
481
|
/** Everything one recipe's manifest declares, under `depends` and `subscribes`. */
|
|
462
482
|
type DeclaredDependencies = {
|
|
463
483
|
/** Every dependency, keyed `namespace/recipe`; a later declaration wins over an earlier one. */
|
|
@@ -487,31 +507,40 @@ function declaredDependencies(
|
|
|
487
507
|
validation: RepoValidation,
|
|
488
508
|
settled: Map<string, SettledDependency> = new Map()
|
|
489
509
|
): DeclaredDependencies {
|
|
490
|
-
const byKey = new Map<string,
|
|
510
|
+
const byKey = new Map<string, DeclaredLocation>();
|
|
491
511
|
const namespaces = new Set<string>();
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
512
|
+
// Which entry brings each recipe in, and whether as a co-subscription, by the
|
|
513
|
+
// rule every reader of the index shares (`foldDeclaration`).
|
|
514
|
+
const declarations = new Map<string, GatheredDeclaration>();
|
|
515
|
+
const declared: ReadonlyArray<readonly [DependencyKind, string]> = [
|
|
516
|
+
...(recipe.manifest.depends ?? []).map((written) => ["depends", written] as const),
|
|
517
|
+
...(recipe.manifest.subscribes ?? []).map((written) => ["subscribes", written] as const),
|
|
495
518
|
];
|
|
496
519
|
|
|
520
|
+
/** Records that one entry covers one recipe. */
|
|
521
|
+
const cover = (key: string, entry: CoveringEntry): void => {
|
|
522
|
+
declarations.set(key, foldDeclaration(declarations.get(key), entry));
|
|
523
|
+
};
|
|
524
|
+
|
|
497
525
|
/** Records one sibling, remembering whether any declaration named it. */
|
|
498
526
|
const addSibling = (key: string, range: string | undefined, named: boolean): void => {
|
|
499
527
|
const before = byKey.get(key);
|
|
500
|
-
const namedBefore = before?.
|
|
528
|
+
const namedBefore = before?.where === "sibling" && before.named;
|
|
501
529
|
byKey.set(key, {
|
|
502
|
-
|
|
530
|
+
where: "sibling",
|
|
503
531
|
...(range === undefined ? {} : { range }),
|
|
504
532
|
named: named || namedBefore,
|
|
505
533
|
});
|
|
506
534
|
};
|
|
507
535
|
|
|
508
|
-
for (const written of declared) {
|
|
536
|
+
for (const [kind, written] of declared) {
|
|
509
537
|
// A dependency that reads more than one way was settled by the release,
|
|
510
538
|
// and what it settled on is what a consumer reads instead of probing.
|
|
511
539
|
const answer = settled.get(written.trim());
|
|
512
540
|
if (answer !== undefined) {
|
|
513
541
|
for (const key of answer.keys) {
|
|
514
|
-
byKey.set(key, {
|
|
542
|
+
byKey.set(key, { where: "remote", repo: answer.identity, range: answer.range ?? "*" });
|
|
543
|
+
cover(key, { written, kind, named: answer.keys.length === 1 });
|
|
515
544
|
}
|
|
516
545
|
continue;
|
|
517
546
|
}
|
|
@@ -533,16 +562,18 @@ function declaredDependencies(
|
|
|
533
562
|
// own index by the consumer; only a recipe has a key to record.
|
|
534
563
|
if (parsed.recipe !== undefined) {
|
|
535
564
|
byKey.set(refKey(parsed), {
|
|
536
|
-
|
|
565
|
+
where: "remote",
|
|
537
566
|
repo: parsed.location.identity,
|
|
538
567
|
range: parsed.range ?? "*",
|
|
539
568
|
});
|
|
569
|
+
cover(refKey(parsed), { written, kind, named: true });
|
|
540
570
|
}
|
|
541
571
|
continue;
|
|
542
572
|
}
|
|
543
573
|
|
|
544
574
|
if (parsed.recipe !== undefined) {
|
|
545
575
|
addSibling(refKey(parsed), parsed.range, true);
|
|
576
|
+
cover(refKey(parsed), { written, kind, named: true });
|
|
546
577
|
continue;
|
|
547
578
|
}
|
|
548
579
|
|
|
@@ -552,10 +583,17 @@ function declaredDependencies(
|
|
|
552
583
|
if (entry.manifest.namespace !== parsed.namespace) continue;
|
|
553
584
|
if (entry.key === recipe.key) continue;
|
|
554
585
|
addSibling(entry.key, undefined, false);
|
|
586
|
+
cover(entry.key, { written, kind, named: false });
|
|
555
587
|
}
|
|
556
588
|
}
|
|
557
589
|
|
|
558
|
-
|
|
590
|
+
const described = new Map<string, DeclaredDependency>();
|
|
591
|
+
for (const [key, dependency] of byKey) {
|
|
592
|
+
const { declared: entry, kind } = declarations.get(key)!;
|
|
593
|
+
described.set(key, { ...dependency, declared: entry, kind });
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return { byKey: described, namespaces };
|
|
559
597
|
}
|
|
560
598
|
|
|
561
599
|
/**
|
|
@@ -628,24 +666,105 @@ function siblingState(
|
|
|
628
666
|
function resolveIndexDependencies(
|
|
629
667
|
declared: DeclaredDependencies,
|
|
630
668
|
siblings: SiblingState
|
|
631
|
-
): Record<string, IndexDependency>
|
|
669
|
+
): Record<string, IndexDependency> {
|
|
632
670
|
const resolved: Record<string, IndexDependency> = {};
|
|
633
671
|
|
|
634
672
|
for (const [key, dependency] of declared.byKey) {
|
|
635
|
-
|
|
636
|
-
|
|
673
|
+
const how = { declared: dependency.declared, kind: dependency.kind };
|
|
674
|
+
if (dependency.where === "remote") {
|
|
675
|
+
resolved[key] = { repo: dependency.repo, range: dependency.range, ...how };
|
|
637
676
|
continue;
|
|
638
677
|
}
|
|
639
678
|
if (dependency.range === undefined) {
|
|
640
679
|
const settled = siblings.settled.get(key);
|
|
641
|
-
resolved[key] =
|
|
680
|
+
resolved[key] = {
|
|
681
|
+
...(settled === undefined ? { range: "*" } : { version: settled }),
|
|
682
|
+
...how,
|
|
683
|
+
};
|
|
642
684
|
continue;
|
|
643
685
|
}
|
|
644
686
|
const best = semver.maxSatisfying(siblings.published.get(key) ?? [], dependency.range);
|
|
645
|
-
resolved[key] =
|
|
687
|
+
resolved[key] = {
|
|
688
|
+
...(best === null ? { range: dependency.range } : { version: best }),
|
|
689
|
+
...how,
|
|
690
|
+
};
|
|
646
691
|
}
|
|
647
692
|
|
|
648
|
-
return
|
|
693
|
+
return resolved;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Everything a version records about itself beyond its hash, tag and date: its
|
|
698
|
+
* dependencies, resolved and each with the entry that declared it, its
|
|
699
|
+
* manifest's `depends` and `subscribes` lists as written, and the variable
|
|
700
|
+
* definitions its manifest publishes. All are recorded even when empty, so a
|
|
701
|
+
* reader can tell "none" from "not recorded".
|
|
702
|
+
*
|
|
703
|
+
* @param recipe - The recipe, with the manifest of the version being recorded.
|
|
704
|
+
* @param declared - What that manifest declares.
|
|
705
|
+
* @param siblings - What each recipe here is published at, for this resolution.
|
|
706
|
+
*/
|
|
707
|
+
function describeVersion(
|
|
708
|
+
recipe: ValidatedRecipe,
|
|
709
|
+
declared: DeclaredDependencies,
|
|
710
|
+
siblings: SiblingState
|
|
711
|
+
): VersionDescription {
|
|
712
|
+
return {
|
|
713
|
+
dependencies: resolveIndexDependencies(declared, siblings),
|
|
714
|
+
depends: (recipe.manifest.depends ?? []).map((written) => written.trim()),
|
|
715
|
+
subscribes: (recipe.manifest.subscribes ?? []).map((written) => written.trim()),
|
|
716
|
+
variables: recipe.manifest.variables ?? [],
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/** What `describeVersion` records about a version. */
|
|
721
|
+
type VersionDescription = Pick<
|
|
722
|
+
IndexVersion,
|
|
723
|
+
"dependencies" | "depends" | "subscribes" | "variables"
|
|
724
|
+
>;
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Checks the manifest lists the index records for an already published
|
|
728
|
+
* version against the version's manifest. They are copied, not resolved, so
|
|
729
|
+
* any difference means the index was edited.
|
|
730
|
+
*
|
|
731
|
+
* @param key - The recipe's key, `namespace/recipe`.
|
|
732
|
+
* @param version - The published version.
|
|
733
|
+
* @param published - The version's index entry.
|
|
734
|
+
* @param recipe - The recipe, with the version's manifest.
|
|
735
|
+
*/
|
|
736
|
+
function checkRecordedLists(
|
|
737
|
+
key: string,
|
|
738
|
+
version: string,
|
|
739
|
+
published: IndexVersion,
|
|
740
|
+
recipe: ValidatedRecipe
|
|
741
|
+
): ValidationProblem[] {
|
|
742
|
+
const differences: string[] = [];
|
|
743
|
+
for (const list of ["depends", "subscribes"] as const) {
|
|
744
|
+
const recorded = published[list];
|
|
745
|
+
if (recorded === undefined) continue;
|
|
746
|
+
const declared = (recipe.manifest[list] ?? []).map((written) => written.trim());
|
|
747
|
+
if (JSON.stringify(recorded) === JSON.stringify(declared)) continue;
|
|
748
|
+
differences.push(
|
|
749
|
+
`'${list}': the index records [${recorded.join(", ")}], and the manifest declares ` +
|
|
750
|
+
`[${declared.join(", ")}]`
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (differences.length === 0) return [];
|
|
755
|
+
return [
|
|
756
|
+
{
|
|
757
|
+
level: "error",
|
|
758
|
+
where: `${INDEX_FILENAME} recipes['${key}'].versions['${version}']`,
|
|
759
|
+
message:
|
|
760
|
+
`version ${version} of '${key}' is already published, and the lists the index ` +
|
|
761
|
+
`records for it disagree with its manifest:\n` +
|
|
762
|
+
differences.map((line) => ` - ${line}\n`).join("") +
|
|
763
|
+
` A published version never changes. If the index was edited, restore the entry; ` +
|
|
764
|
+
`to publish different lists, bump the version in this manifest, or with ` +
|
|
765
|
+
`'sous repo release --bump patch'.`,
|
|
766
|
+
},
|
|
767
|
+
];
|
|
649
768
|
}
|
|
650
769
|
|
|
651
770
|
/**
|
|
@@ -679,7 +798,7 @@ function checkRecordedDependencies(
|
|
|
679
798
|
for (const [name, dependency] of declared.byKey) {
|
|
680
799
|
const entry = entries[name];
|
|
681
800
|
if (entry === undefined) {
|
|
682
|
-
if (dependency.
|
|
801
|
+
if (dependency.where === "remote" || dependency.named) {
|
|
683
802
|
differences.push(
|
|
684
803
|
`'${name}': the manifest declares ${describeDeclared(dependency)}, and the index ` +
|
|
685
804
|
`records nothing for it`
|
|
@@ -726,7 +845,11 @@ function checkRecordedDependencies(
|
|
|
726
845
|
|
|
727
846
|
/** True when a recorded dependency honours what the manifest declares for it. */
|
|
728
847
|
function recordedSatisfies(entry: IndexDependency, dependency: DeclaredDependency): boolean {
|
|
729
|
-
|
|
848
|
+
// How it was declared is recorded only since sous described recipes in the
|
|
849
|
+
// index; an entry that records it has to agree with the manifest too.
|
|
850
|
+
if (entry.declared !== undefined && entry.declared !== dependency.declared) return false;
|
|
851
|
+
if (entry.kind !== undefined && entry.kind !== dependency.kind) return false;
|
|
852
|
+
if (dependency.where === "remote") {
|
|
730
853
|
return (
|
|
731
854
|
entry.repo === dependency.repo &&
|
|
732
855
|
entry.range === dependency.range &&
|
|
@@ -743,23 +866,34 @@ function recordedSatisfies(entry: IndexDependency, dependency: DeclaredDependenc
|
|
|
743
866
|
function describeRecorded(entry: IndexDependency): string {
|
|
744
867
|
const what =
|
|
745
868
|
entry.version !== undefined ? `version ${entry.version}` : `the range '${entry.range}'`;
|
|
746
|
-
|
|
869
|
+
const from = entry.repo === undefined ? what : `${what} from ${entry.repo}`;
|
|
870
|
+
return entry.declared === undefined || entry.kind === undefined
|
|
871
|
+
? from
|
|
872
|
+
: `${from}, through the entry '${entry.declared}' as a ${describeDependencyKind(entry.kind)}`;
|
|
747
873
|
}
|
|
748
874
|
|
|
749
875
|
/** Describes a declared dependency the way an author would say it. */
|
|
750
876
|
function describeDeclared(dependency: DeclaredDependency): string {
|
|
751
|
-
|
|
752
|
-
|
|
877
|
+
const how =
|
|
878
|
+
`, through the entry '${dependency.declared}' as a ` +
|
|
879
|
+
describeDependencyKind(dependency.kind);
|
|
880
|
+
if (dependency.where === "remote") {
|
|
881
|
+
return `the range '${dependency.range}' from ${dependency.repo}${how}`;
|
|
753
882
|
}
|
|
754
|
-
return dependency.range === undefined
|
|
883
|
+
return dependency.range === undefined
|
|
884
|
+
? `it with no range${how}`
|
|
885
|
+
: `the range '${dependency.range}'${how}`;
|
|
755
886
|
}
|
|
756
887
|
|
|
757
888
|
/** What rebuilding one tagged version from its tag produced. */
|
|
758
889
|
type RebuiltVersion = {
|
|
759
890
|
/** The content hash of the recipe folder at the tag. */
|
|
760
891
|
hash: string;
|
|
761
|
-
/**
|
|
762
|
-
|
|
892
|
+
/**
|
|
893
|
+
* Its dependencies and variable definitions when the recipe could be read at
|
|
894
|
+
* the tag, and nothing when it could not; see `describeVersion`.
|
|
895
|
+
*/
|
|
896
|
+
description: VersionDescription;
|
|
763
897
|
/** A warning when they could not. */
|
|
764
898
|
problems: ValidationProblem[];
|
|
765
899
|
};
|
|
@@ -800,11 +934,12 @@ async function rebuildTaggedVersion(
|
|
|
800
934
|
const snapshot = validateRepo(treeDir);
|
|
801
935
|
const tagged = snapshot.recipes.find((entry) => entry.key === recipe.key);
|
|
802
936
|
if (tagged !== undefined) {
|
|
803
|
-
const
|
|
937
|
+
const description = describeVersion(
|
|
938
|
+
tagged,
|
|
804
939
|
declaredDependencies(tagged, snapshot, settled),
|
|
805
940
|
siblingState(snapshot, tagsByKey, existing)
|
|
806
941
|
);
|
|
807
|
-
return { hash,
|
|
942
|
+
return { hash, description, problems: [] };
|
|
808
943
|
}
|
|
809
944
|
reason = `The repository manifest at that tag does not list '${recipe.key}'.`;
|
|
810
945
|
} catch (error) {
|
|
@@ -813,6 +948,7 @@ async function rebuildTaggedVersion(
|
|
|
813
948
|
|
|
814
949
|
return {
|
|
815
950
|
hash,
|
|
951
|
+
description: {},
|
|
816
952
|
problems: [
|
|
817
953
|
{
|
|
818
954
|
level: "warning",
|
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
* project) and `subscribes` (co-subscribed with full semantics), and those refs
|
|
14
14
|
* resolve the same way. The manifest lives inside the recipe's own files, so
|
|
15
15
|
* the caller supplies a loader; that is also the seam where a caller decides
|
|
16
|
-
* whether it is willing to fetch anything at this point.
|
|
16
|
+
* whether it is willing to fetch anything at this point. When the loader has no
|
|
17
|
+
* manifest to give, the lists are read from the version's index entry instead,
|
|
18
|
+
* which records the entry that declared each dependency; only a version whose
|
|
19
|
+
* entry predates that record goes unwalked.
|
|
17
20
|
*
|
|
18
21
|
* Two things the resolver deliberately does NOT do: it never downloads
|
|
19
22
|
* anything, and it never resolves against a repository the project has not
|
|
@@ -25,7 +28,8 @@ import semver from "semver";
|
|
|
25
28
|
import { ConfigError } from "../errors.js";
|
|
26
29
|
import type { IndexFile } from "./formats/index-file.js";
|
|
27
30
|
import type { LockKind } from "./formats/lockfile.js";
|
|
28
|
-
import type { RecipeManifest } from "./formats/recipe-manifest.js";
|
|
31
|
+
import type { RecipeManifest, VariableDefinition } from "./formats/recipe-manifest.js";
|
|
32
|
+
import { indexDependencyLists, type DependencyLists } from "./declarations.js";
|
|
29
33
|
import {
|
|
30
34
|
formatRef,
|
|
31
35
|
isBrowsedReading,
|
|
@@ -81,8 +85,9 @@ export type ResolverRepo = {
|
|
|
81
85
|
|
|
82
86
|
/**
|
|
83
87
|
* Loads a resolved recipe's manifest, which is what names its dependencies.
|
|
84
|
-
* Returning undefined means "not available"
|
|
85
|
-
*
|
|
88
|
+
* Returning undefined means "not available": the recipe's dependencies are then
|
|
89
|
+
* read from its index entry, and when that does not record them either, it is
|
|
90
|
+
* reported under `missingManifests` rather than having its dependencies walked.
|
|
86
91
|
*/
|
|
87
92
|
export type RecipeManifestLoader = (
|
|
88
93
|
recipe: ResolvedRecipe
|
|
@@ -141,6 +146,17 @@ export type ResolvedRecipe = {
|
|
|
141
146
|
* published against rather than at whatever its range would reach today.
|
|
142
147
|
*/
|
|
143
148
|
dependencies?: Record<string, IndexDependency>;
|
|
149
|
+
/**
|
|
150
|
+
* The variable definitions the index records for this exact version, when it
|
|
151
|
+
* records them: the questions it asks, known before its files are fetched.
|
|
152
|
+
*/
|
|
153
|
+
variables?: VariableDefinition[];
|
|
154
|
+
/**
|
|
155
|
+
* The manifest `depends` and `subscribes` lists the index records for this
|
|
156
|
+
* exact version, when it records them, so the recipe can be walked before its
|
|
157
|
+
* files are fetched.
|
|
158
|
+
*/
|
|
159
|
+
declares?: DependencyLists;
|
|
144
160
|
};
|
|
145
161
|
|
|
146
162
|
/** A repository something needs that the project has not added. */
|
|
@@ -166,7 +182,10 @@ export type ResolveResult = {
|
|
|
166
182
|
resolved: ResolvedRecipe[];
|
|
167
183
|
/** Repositories a dependency needs that the project has not added. */
|
|
168
184
|
missingRepos: MissingRepo[];
|
|
169
|
-
/**
|
|
185
|
+
/**
|
|
186
|
+
* Resolved recipes whose dependencies could not be read: the loader produced
|
|
187
|
+
* no manifest, and the index entry does not record them.
|
|
188
|
+
*/
|
|
170
189
|
missingManifests: string[];
|
|
171
190
|
/** Any dependency cycle found, as the chain of recipe keys forming it. */
|
|
172
191
|
cycles: string[][];
|
|
@@ -495,15 +514,15 @@ export async function resolveRefs(
|
|
|
495
514
|
}
|
|
496
515
|
walked.set(recipe.key, recipe.version);
|
|
497
516
|
|
|
498
|
-
const
|
|
499
|
-
if (
|
|
517
|
+
const lists = dependencyListsOf(recipe, await context.loadManifest(recipe));
|
|
518
|
+
if (lists === undefined) {
|
|
500
519
|
missingManifests.add(recipe.key);
|
|
501
520
|
continue;
|
|
502
521
|
}
|
|
503
522
|
|
|
504
523
|
for (const [kind, refs] of [
|
|
505
|
-
["depends",
|
|
506
|
-
["subscribes",
|
|
524
|
+
["depends", lists.depends],
|
|
525
|
+
["subscribes", lists.subscribes],
|
|
507
526
|
] as Array<[LockKind, string[]]>) {
|
|
508
527
|
for (const written of refs) {
|
|
509
528
|
queue.push(dependencyRequest(written, recipe, kind, context));
|
|
@@ -593,8 +612,9 @@ async function keepOnlyReachable(
|
|
|
593
612
|
} catch {
|
|
594
613
|
manifest = undefined;
|
|
595
614
|
}
|
|
615
|
+
const lists = dependencyListsOf(recipe, manifest);
|
|
596
616
|
|
|
597
|
-
if (
|
|
617
|
+
if (lists === undefined) {
|
|
598
618
|
// An unreadable manifest is already reported as a missing manifest. Keep
|
|
599
619
|
// everything this recipe held rather than dropping a dependency over it.
|
|
600
620
|
for (const other of resolved.values()) {
|
|
@@ -606,8 +626,8 @@ async function keepOnlyReachable(
|
|
|
606
626
|
}
|
|
607
627
|
|
|
608
628
|
for (const [kind, refs] of [
|
|
609
|
-
["depends",
|
|
610
|
-
["subscribes",
|
|
629
|
+
["depends", lists.depends],
|
|
630
|
+
["subscribes", lists.subscribes],
|
|
611
631
|
] as Array<[LockKind, string[]]>) {
|
|
612
632
|
for (const written of refs) {
|
|
613
633
|
for (const target of dependencyTargets(written, recipe, context, resolved)) {
|
|
@@ -663,6 +683,24 @@ function dependencyTargets(
|
|
|
663
683
|
: [refKey(item.ref)];
|
|
664
684
|
}
|
|
665
685
|
|
|
686
|
+
/**
|
|
687
|
+
* What a resolved recipe declares under `depends` and `subscribes`: its
|
|
688
|
+
* manifest's lists when the manifest could be read, and otherwise the lists its
|
|
689
|
+
* index entry records, or undefined when neither says.
|
|
690
|
+
*
|
|
691
|
+
* @param recipe - The resolved recipe, carrying the lists its index entry records.
|
|
692
|
+
* @param manifest - Its manifest, when the loader produced one.
|
|
693
|
+
*/
|
|
694
|
+
function dependencyListsOf(
|
|
695
|
+
recipe: ResolvedRecipe,
|
|
696
|
+
manifest: RecipeManifest | undefined
|
|
697
|
+
): DependencyLists | undefined {
|
|
698
|
+
if (manifest !== undefined) {
|
|
699
|
+
return { depends: manifest.depends ?? [], subscribes: manifest.subscribes ?? [] };
|
|
700
|
+
}
|
|
701
|
+
return recipe.declares;
|
|
702
|
+
}
|
|
703
|
+
|
|
666
704
|
/** Records one more reason a repository is needed. */
|
|
667
705
|
function recordMissingRepo(into: Map<string, MissingRepo>, missing: MissingRepo): void {
|
|
668
706
|
const existing = into.get(missing.name);
|
|
@@ -775,6 +813,7 @@ function resolveRecipeRef(
|
|
|
775
813
|
|
|
776
814
|
const version = pickVersion(key, entry, ranges, prerelease, context.keep?.[key]);
|
|
777
815
|
const versionEntry = entry.versions[version]!;
|
|
816
|
+
const declares = indexDependencyLists(versionEntry);
|
|
778
817
|
|
|
779
818
|
const requestedBy = [...(previous?.requestedBy ?? [])];
|
|
780
819
|
if (!requestedBy.includes(item.requestedBy)) requestedBy.push(item.requestedBy);
|
|
@@ -793,6 +832,8 @@ function resolveRecipeRef(
|
|
|
793
832
|
...(versionEntry.dependencies === undefined
|
|
794
833
|
? {}
|
|
795
834
|
: { dependencies: versionEntry.dependencies }),
|
|
835
|
+
...(versionEntry.variables === undefined ? {} : { variables: versionEntry.variables }),
|
|
836
|
+
...(declares === undefined ? {} : { declares }),
|
|
796
837
|
// A recipe held as a co-subscription by anyone is a co-subscription; a
|
|
797
838
|
// build dependency only stays one while nothing subscribes to it.
|
|
798
839
|
kind: previous?.kind === "subscribes" || item.kind === "subscribes" ? "subscribes" : "depends",
|
|
@@ -56,7 +56,7 @@ import {
|
|
|
56
56
|
type LockedRecipe,
|
|
57
57
|
type Lockfile,
|
|
58
58
|
} from "./formats/lockfile.js";
|
|
59
|
-
import type { RecipeManifest } from "./formats/recipe-manifest.js";
|
|
59
|
+
import type { RecipeManifest, VariableDefinition } from "./formats/recipe-manifest.js";
|
|
60
60
|
import {
|
|
61
61
|
RefSource,
|
|
62
62
|
Qualification,
|
|
@@ -292,9 +292,9 @@ export type SubscribeOutcome = {
|
|
|
292
292
|
*/
|
|
293
293
|
questions?: PlannedVariable[];
|
|
294
294
|
/**
|
|
295
|
-
* Recipes a dry run could not describe
|
|
296
|
-
*
|
|
297
|
-
* they are installed.
|
|
295
|
+
* Recipes a dry run could not describe: their files are not on this machine,
|
|
296
|
+
* a dry run downloads nothing, and their repository's index does not record
|
|
297
|
+
* their questions. Their questions are unknown until they are installed.
|
|
298
298
|
*/
|
|
299
299
|
unreadable?: string[];
|
|
300
300
|
/** Dependency cycles the resolver noticed, reported rather than treated as fatal. */
|
|
@@ -303,6 +303,28 @@ export type SubscribeOutcome = {
|
|
|
303
303
|
dryRun: boolean;
|
|
304
304
|
};
|
|
305
305
|
|
|
306
|
+
/** What `previewSubscription` is asked about. */
|
|
307
|
+
export type PreviewSubscriptionOptions = {
|
|
308
|
+
/** The indexes to resolve against, keyed by short name; the cached ones by default. */
|
|
309
|
+
indexes?: Map<string, IndexFile>;
|
|
310
|
+
/** Whether prerelease versions may take part in range matching. */
|
|
311
|
+
prerelease?: boolean;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
/** What subscribing to a ref would install and ask, worked out without changing anything. */
|
|
315
|
+
export type SubscriptionPreview = {
|
|
316
|
+
/** Every recipe version the subscription would install, sorted by key. */
|
|
317
|
+
resolved: ResolvedRecipe[];
|
|
318
|
+
/** Repositories the closure needs that the project does not trust yet. */
|
|
319
|
+
missingRepos: MissingRepo[];
|
|
320
|
+
/** Every question the closure would ask, and where each answer would go. */
|
|
321
|
+
questions: PlannedVariable[];
|
|
322
|
+
/** Recipes whose questions cannot be listed; see `SubscribeOutcome.unreadable`. */
|
|
323
|
+
unreadable: string[];
|
|
324
|
+
/** Dependency cycles the resolver noticed. */
|
|
325
|
+
cycles: string[][];
|
|
326
|
+
};
|
|
327
|
+
|
|
306
328
|
/** What `unsubscribe` is asked to do. */
|
|
307
329
|
export type UnsubscribeOptions = {
|
|
308
330
|
/** The ref to unsubscribe from. */
|
|
@@ -3529,9 +3551,10 @@ export class SubscriptionService {
|
|
|
3529
3551
|
* Every variable definition the resolved closure publishes, attributed to the
|
|
3530
3552
|
* recipe that declared it and to the chain that pulled it in.
|
|
3531
3553
|
*
|
|
3532
|
-
* A recipe whose files are not on this machine
|
|
3533
|
-
*
|
|
3534
|
-
*
|
|
3554
|
+
* A recipe whose files are not on this machine is described from the
|
|
3555
|
+
* definitions its index entry records, and contributes nothing rather than
|
|
3556
|
+
* failing when the entry records none, which is what lets a dry run describe
|
|
3557
|
+
* as much of the closure as it can without downloading any of it.
|
|
3535
3558
|
*
|
|
3536
3559
|
* @param resolved - The recipe versions the resolution settled on.
|
|
3537
3560
|
*/
|
|
@@ -3581,12 +3604,12 @@ export class SubscriptionService {
|
|
|
3581
3604
|
});
|
|
3582
3605
|
|
|
3583
3606
|
for (const recipe of ordered) {
|
|
3584
|
-
const
|
|
3585
|
-
if (
|
|
3607
|
+
const variables = this.publishedVariables(recipe);
|
|
3608
|
+
if (variables === undefined) continue;
|
|
3586
3609
|
|
|
3587
3610
|
const publisher = describe(recipe);
|
|
3588
3611
|
const requiredBy = chainFor(recipe).map(describe);
|
|
3589
|
-
for (const definition of
|
|
3612
|
+
for (const definition of variables) {
|
|
3590
3613
|
defined.push({ definition, recipe: publisher, requiredBy });
|
|
3591
3614
|
}
|
|
3592
3615
|
}
|
|
@@ -3604,11 +3627,70 @@ export class SubscriptionService {
|
|
|
3604
3627
|
*/
|
|
3605
3628
|
private unreadableRecipes(resolved: ResolvedRecipe[]): string[] {
|
|
3606
3629
|
return resolved
|
|
3607
|
-
.filter((recipe) =>
|
|
3630
|
+
.filter((recipe) => this.publishedVariables(recipe) === undefined)
|
|
3608
3631
|
.map((recipe) => recipe.key)
|
|
3609
3632
|
.sort();
|
|
3610
3633
|
}
|
|
3611
3634
|
|
|
3635
|
+
/**
|
|
3636
|
+
* The variable definitions one resolved recipe publishes: from its manifest
|
|
3637
|
+
* when its files are on this machine (a linked checkout's working copy
|
|
3638
|
+
* included), otherwise from its index entry, or undefined when neither can
|
|
3639
|
+
* say.
|
|
3640
|
+
*
|
|
3641
|
+
* @param recipe - The resolved recipe.
|
|
3642
|
+
*/
|
|
3643
|
+
private publishedVariables(recipe: ResolvedRecipe): VariableDefinition[] | undefined {
|
|
3644
|
+
const manifest = readRecipeManifestIn(this.recipeDirectory(recipe));
|
|
3645
|
+
if (manifest !== undefined) return manifest.variables ?? [];
|
|
3646
|
+
return recipe.variables;
|
|
3647
|
+
}
|
|
3648
|
+
|
|
3649
|
+
/**
|
|
3650
|
+
* Works out what subscribing to a ref would install and which questions it
|
|
3651
|
+
* would ask, from the indexes and the files already on this machine. Nothing
|
|
3652
|
+
* is fetched, trusted or written, which is what lets `sous recipe show`
|
|
3653
|
+
* describe a recipe before anyone subscribes to it. A repository the closure
|
|
3654
|
+
* needs and the project does not trust is reported, and its recipes are not
|
|
3655
|
+
* resolved.
|
|
3656
|
+
*
|
|
3657
|
+
* @param parsed - The fully qualified ref, with the exact version to describe as its range.
|
|
3658
|
+
* @param options - The indexes to read, and whether prereleases may match.
|
|
3659
|
+
*/
|
|
3660
|
+
async previewSubscription(
|
|
3661
|
+
parsed: ParsedRef,
|
|
3662
|
+
options: PreviewSubscriptionOptions = {}
|
|
3663
|
+
): Promise<SubscriptionPreview> {
|
|
3664
|
+
const repos = this.resolverRepos();
|
|
3665
|
+
const indexes = options.indexes ?? (await this.loadIndexes(Object.keys(repos)));
|
|
3666
|
+
const result = await resolveRefs(
|
|
3667
|
+
[
|
|
3668
|
+
{
|
|
3669
|
+
ref: parsed,
|
|
3670
|
+
requestedBy: PROJECT_REQUESTER,
|
|
3671
|
+
kind: "subscribes",
|
|
3672
|
+
...(options.prerelease === true ? { prerelease: true } : {}),
|
|
3673
|
+
},
|
|
3674
|
+
],
|
|
3675
|
+
{
|
|
3676
|
+
indexes,
|
|
3677
|
+
repos,
|
|
3678
|
+
loadManifest: (recipe) => this.loadRecipeManifest(recipe, true),
|
|
3679
|
+
...(options.prerelease === true ? { prerelease: true } : {}),
|
|
3680
|
+
}
|
|
3681
|
+
);
|
|
3682
|
+
|
|
3683
|
+
return {
|
|
3684
|
+
resolved: result.resolved,
|
|
3685
|
+
missingRepos: result.missingRepos,
|
|
3686
|
+
questions: planQuestions(this.definedVariables(result.resolved), this.ladderContext(), {
|
|
3687
|
+
sousDir: this.sousDir,
|
|
3688
|
+
}),
|
|
3689
|
+
unreadable: this.unreadableRecipes(result.resolved),
|
|
3690
|
+
cycles: result.cycles,
|
|
3691
|
+
};
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3612
3694
|
/** The environment layers and mapping records this project resolves against. */
|
|
3613
3695
|
private ladderContext(): LadderContext {
|
|
3614
3696
|
return loadLadderContext({
|