@sous-io/sous 0.2.36 → 1.0.1
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 +14 -256
- package/dist/bin.js +11 -0
- package/dist/sous.js +1093 -0
- package/docs/markdown/README.md +5 -4
- package/docs/markdown/_sidebar.md +21 -1
- package/docs/markdown/commands.md +294 -431
- package/docs/markdown/config-discovery.md +25 -14
- package/docs/markdown/config-inspection.md +53 -31
- package/docs/markdown/config-layers.md +168 -66
- package/docs/markdown/config-variables.md +44 -11
- package/docs/markdown/configuration.md +104 -17
- package/docs/markdown/design-principles.md +4 -1
- package/docs/markdown/repositories-authoring.md +30 -0
- package/docs/markdown/repositories-consuming.md +72 -14
- package/docs/markdown/repositories-file-formats.md +31 -13
- package/docs/markdown/repositories-providers.md +48 -57
- package/docs/markdown/repositories-quickstart.md +3 -2
- package/docs/markdown/repositories-troubleshooting.md +27 -1
- package/docs/markdown/repositories-variables.md +6 -1
- package/docs/markdown/repositories.md +18 -8
- package/package.json +48 -107
- package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +4 -6
- package/recipes/core/sous-skills/skills/about-liquid-templates/references/liquid-filters.tpl.md +2 -0
- package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +10 -9
- package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +35 -14
- package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +51 -20
- package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
- package/sous.config.schema.json +351 -132
- package/bin/run.js +0 -51
- package/src/base-command.ts +0 -457
- package/src/commands/build.ts +0 -181
- package/src/commands/clear.ts +0 -79
- package/src/commands/compile.ts +0 -155
- package/src/commands/config/get.ts +0 -110
- package/src/commands/config/show.ts +0 -32
- package/src/commands/config/validate.ts +0 -53
- package/src/commands/help.ts +0 -46
- package/src/commands/init.ts +0 -289
- package/src/commands/launch.ts +0 -139
- package/src/commands/lock/rebuild.ts +0 -239
- package/src/commands/lock/show.ts +0 -115
- package/src/commands/namespace/list.ts +0 -138
- package/src/commands/namespace/show.ts +0 -130
- package/src/commands/prune.ts +0 -43
- package/src/commands/recipe/list.ts +0 -112
- package/src/commands/recipe/show.ts +0 -439
- package/src/commands/repo/add.ts +0 -145
- package/src/commands/repo/contribute.ts +0 -604
- package/src/commands/repo/gc.ts +0 -173
- package/src/commands/repo/init.ts +0 -136
- package/src/commands/repo/link.ts +0 -899
- package/src/commands/repo/list.ts +0 -236
- package/src/commands/repo/release.ts +0 -685
- package/src/commands/repo/remove.ts +0 -194
- package/src/commands/repo/search.ts +0 -243
- package/src/commands/repo/submit.ts +0 -341
- package/src/commands/repo/unlink.ts +0 -461
- package/src/commands/subscription/add.ts +0 -286
- package/src/commands/subscription/list.ts +0 -208
- package/src/commands/subscription/remove.ts +0 -182
- package/src/commands/subscription/update.ts +0 -216
- package/src/commands/update.ts +0 -178
- package/src/commands/vars/ask.ts +0 -417
- package/src/commands/vars/index.ts +0 -79
- package/src/commands/vars/list.ts +0 -67
- package/src/commands/vars/show.ts +0 -77
- package/src/config-command.ts +0 -30
- package/src/lib/build-preparation.ts +0 -128
- package/src/lib/build-service.ts +0 -580
- package/src/lib/config-discovery.ts +0 -378
- package/src/lib/config-inspect.ts +0 -145
- package/src/lib/config-kernel.mjs +0 -379
- package/src/lib/config-schema.ts +0 -401
- package/src/lib/env-file.ts +0 -328
- package/src/lib/env-local.ts +0 -212
- package/src/lib/errors.ts +0 -32
- package/src/lib/glob-pattern.ts +0 -24
- package/src/lib/include-resolver.ts +0 -523
- package/src/lib/interactive.ts +0 -165
- package/src/lib/markdown-compiler.ts +0 -947
- package/src/lib/package-info.ts +0 -25
- package/src/lib/pid-service.ts +0 -99
- package/src/lib/project-install.d.mts +0 -44
- package/src/lib/project-install.mjs +0 -234
- package/src/lib/project-scaffold/index.ts +0 -332
- package/src/lib/project-scaffold/templates.ts +0 -240
- package/src/lib/repos/catalog-display.ts +0 -215
- package/src/lib/repos/catalog-inputs.ts +0 -288
- package/src/lib/repos/catalog.ts +0 -842
- package/src/lib/repos/contribute.ts +0 -452
- package/src/lib/repos/core-recipe.ts +0 -105
- package/src/lib/repos/declarations.ts +0 -187
- package/src/lib/repos/defaults.ts +0 -175
- package/src/lib/repos/formats/common.ts +0 -440
- package/src/lib/repos/formats/index-file.ts +0 -253
- package/src/lib/repos/formats/links-map.ts +0 -98
- package/src/lib/repos/formats/lockfile.ts +0 -169
- package/src/lib/repos/formats/patterns.ts +0 -57
- package/src/lib/repos/formats/recipe-manifest.ts +0 -205
- package/src/lib/repos/formats/repo-manifest.ts +0 -97
- package/src/lib/repos/formats/store-entry.ts +0 -84
- package/src/lib/repos/formats/variable-definition.ts +0 -268
- package/src/lib/repos/freshness.ts +0 -264
- package/src/lib/repos/git-clone.ts +0 -850
- package/src/lib/repos/identity.ts +0 -89
- package/src/lib/repos/index.ts +0 -57
- package/src/lib/repos/links.ts +0 -356
- package/src/lib/repos/load-manifest.ts +0 -237
- package/src/lib/repos/lock-service.ts +0 -454
- package/src/lib/repos/locked-namespace-resolver.ts +0 -232
- package/src/lib/repos/locked-recipe-lookup.ts +0 -123
- package/src/lib/repos/locked-recipes.ts +0 -254
- package/src/lib/repos/managed-layer.ts +0 -422
- package/src/lib/repos/memories-hash-name.ts +0 -33
- package/src/lib/repos/namespace-resolver.ts +0 -527
- package/src/lib/repos/providers/base.ts +0 -268
- package/src/lib/repos/providers/git.ts +0 -237
- package/src/lib/repos/providers/github.ts +0 -589
- package/src/lib/repos/providers/gitlab.ts +0 -332
- package/src/lib/repos/providers/http.ts +0 -128
- package/src/lib/repos/providers/index-cache.ts +0 -421
- package/src/lib/repos/providers/index.ts +0 -106
- package/src/lib/repos/providers/local.ts +0 -391
- package/src/lib/repos/providers/provider.ts +0 -615
- package/src/lib/repos/recipe-config-layers.ts +0 -316
- package/src/lib/repos/recipe-key-matcher.ts +0 -62
- package/src/lib/repos/recipe-memories.ts +0 -178
- package/src/lib/repos/recipe-targets.ts +0 -236
- package/src/lib/repos/ref-search.ts +0 -46
- package/src/lib/repos/release/bump.ts +0 -161
- package/src/lib/repos/release/changelog.ts +0 -461
- package/src/lib/repos/release/git-state.ts +0 -460
- package/src/lib/repos/release/index-builder.ts +0 -1108
- package/src/lib/repos/release/index.ts +0 -19
- package/src/lib/repos/release/plan.ts +0 -521
- package/src/lib/repos/release/settle.ts +0 -154
- package/src/lib/repos/release/submissions.ts +0 -214
- package/src/lib/repos/release/submit-checkout.ts +0 -290
- package/src/lib/repos/release/submit-questions.ts +0 -153
- package/src/lib/repos/release/submit-service.ts +0 -898
- package/src/lib/repos/release/tags.ts +0 -239
- package/src/lib/repos/release/validate.ts +0 -463
- package/src/lib/repos/resolver.ts +0 -1028
- package/src/lib/repos/scaffold/index.ts +0 -245
- package/src/lib/repos/scaffold/templates.ts +0 -517
- package/src/lib/repos/seed.ts +0 -473
- package/src/lib/repos/store/contract.ts +0 -64
- package/src/lib/repos/store/hash.ts +0 -191
- package/src/lib/repos/store/recipe-store.ts +0 -599
- package/src/lib/repos/store/settings.ts +0 -58
- package/src/lib/repos/subscription-service.ts +0 -3859
- package/src/lib/repos/trust.ts +0 -447
- package/src/lib/repos/update-plan.ts +0 -235
- package/src/lib/self-update/index.ts +0 -14
- package/src/lib/self-update/installs.ts +0 -412
- package/src/lib/self-update/managers.ts +0 -126
- package/src/lib/self-update/plan.ts +0 -315
- package/src/lib/self-update/registry.ts +0 -99
- package/src/lib/self-update/run.ts +0 -506
- package/src/lib/self-update/versions.ts +0 -243
- package/src/lib/settings.ts +0 -1105
- package/src/lib/sous-home.ts +0 -104
- package/src/lib/state.ts +0 -167
- package/src/lib/vars/answers.ts +0 -192
- package/src/lib/vars/ask.ts +0 -1152
- package/src/lib/vars/definition-source.ts +0 -261
- package/src/lib/vars/display.ts +0 -231
- package/src/lib/vars/index.ts +0 -20
- package/src/lib/vars/ladder.ts +0 -282
- package/src/lib/vars/mappings.ts +0 -260
- package/src/lib/vars/names.ts +0 -94
- package/src/lib/vars/preanswers.ts +0 -395
- package/src/lib/vars/question-plan.ts +0 -241
- package/src/lib/vars/report.ts +0 -228
- package/src/lib/vars/safe-regex.ts +0 -235
- package/src/lib/vars/secrets.ts +0 -131
- package/src/lib/vars/validate.ts +0 -312
- package/src/lib/version-report.ts +0 -90
- package/src/lib/watch-loop.ts +0 -148
- package/src/lib/watch-service.ts +0 -116
- package/src/services/ref-resolver/container.ts +0 -107
- package/src/services/ref-resolver/dependency.ts +0 -60
- package/src/services/ref-resolver/format.ts +0 -197
- package/src/services/ref-resolver/glob.ts +0 -115
- package/src/services/ref-resolver/hash-names.ts +0 -155
- package/src/services/ref-resolver/index.ts +0 -55
- package/src/services/ref-resolver/injectable.ts +0 -36
- package/src/services/ref-resolver/location.ts +0 -62
- package/src/services/ref-resolver/lookups/cached-index-lookup.ts +0 -53
- package/src/services/ref-resolver/lookups/catalog-lookup.ts +0 -23
- package/src/services/ref-resolver/lookups/catalog-matcher.ts +0 -261
- package/src/services/ref-resolver/lookups/chained-lookup.ts +0 -27
- package/src/services/ref-resolver/lookups/env-var-lookup.ts +0 -65
- package/src/services/ref-resolver/lookups/fetched-index-lookup.ts +0 -150
- package/src/services/ref-resolver/lookups/recorded-dependency-lookup.ts +0 -62
- package/src/services/ref-resolver/lookups/ref-lookup.ts +0 -54
- package/src/services/ref-resolver/lookups/variable-lookup.ts +0 -123
- package/src/services/ref-resolver/named-refs.ts +0 -144
- package/src/services/ref-resolver/parser/env-var-splitter.ts +0 -27
- package/src/services/ref-resolver/parser/location-splitter.ts +0 -239
- package/src/services/ref-resolver/parser/name-path-splitter.ts +0 -200
- package/src/services/ref-resolver/parser/partial-ref.ts +0 -84
- package/src/services/ref-resolver/parser/query-splitter.ts +0 -61
- package/src/services/ref-resolver/parser/range-splitter.ts +0 -52
- package/src/services/ref-resolver/parser/ref-parser.ts +0 -115
- package/src/services/ref-resolver/parser/repo-qualifier-splitter.ts +0 -51
- package/src/services/ref-resolver/parts.ts +0 -115
- package/src/services/ref-resolver/pruners/command-line-pruner.ts +0 -47
- package/src/services/ref-resolver/pruners/include-pruner.ts +0 -66
- package/src/services/ref-resolver/pruners/manifest-pruner.ts +0 -130
- package/src/services/ref-resolver/pruners/ref-pruner.ts +0 -135
- package/src/services/ref-resolver/pruners/rule-helpers.ts +0 -44
- package/src/services/ref-resolver/pruners/stored-key-pruner.ts +0 -136
- package/src/services/ref-resolver/ref-helpers.ts +0 -62
- package/src/services/ref-resolver/ref-picker-service.ts +0 -204
- package/src/services/ref-resolver/ref-report.ts +0 -84
- package/src/services/ref-resolver/ref-resolve-arguments.ts +0 -47
- package/src/services/ref-resolver/ref-resolve-result.ts +0 -36
- package/src/services/ref-resolver/ref-resolver-service.ts +0 -323
- package/src/services/ref-resolver/source.ts +0 -48
- package/src/services/ref-resolver/tokens.ts +0 -25
- package/src/services/ref-resolver/types.ts +0 -155
- package/src/templating/filters/bullet-list.ts +0 -9
- package/src/templating/filters/index.ts +0 -8
- package/src/templating/init-liquid-engine.ts +0 -139
- package/src/templating/lib/glob-files.ts +0 -74
- package/src/templating/lib/import-export.ts +0 -32
- package/src/templating/lib/secret-scope.ts +0 -41
- package/src/templating/lib/tag-args.ts +0 -19
- package/src/templating/tags/exportScalarVarsJs.ts +0 -46
- package/src/templating/tags/getFiles.ts +0 -89
- package/src/templating/tags/index.ts +0 -14
- package/src/templating/tags/listFiles.ts +0 -54
- package/src/templating/tags/showVars.ts +0 -28
- package/src/utils/choice-prompt.ts +0 -143
- package/src/utils/command-errors.ts +0 -186
- package/src/utils/command-help.ts +0 -45
- package/src/utils/confirm-prompt.ts +0 -110
- package/src/utils/flags.ts +0 -177
- package/src/utils/formatting.ts +0 -849
- package/src/utils/prompts.ts +0 -53
- package/src/utils/sous-directory.ts +0 -245
- package/src/utils/table.ts +0 -603
- package/src/utils/value-prompt.ts +0 -119
|
@@ -1,1028 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The resolver.
|
|
3
|
-
*
|
|
4
|
-
* Given what a project asked for (its subscriptions, or the refs named on a
|
|
5
|
-
* command line), the resolver decides exactly which recipe versions that means.
|
|
6
|
-
* It works the way apt does: a ref is looked up across the cached indexes of
|
|
7
|
-
* EVERY added repository at once, and a ref that genuinely exists in more than
|
|
8
|
-
* one of them is an error demanding the qualified form, never a silent
|
|
9
|
-
* first-match-wins.
|
|
10
|
-
*
|
|
11
|
-
* It then walks the dependency closure. Each resolved recipe's manifest names
|
|
12
|
-
* more refs, through `depends` (fetched and addressable, but not added to the
|
|
13
|
-
* project) and `subscribes` (co-subscribed with full semantics), and those refs
|
|
14
|
-
* resolve the same way. The manifest lives inside the recipe's own files, so
|
|
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. 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.
|
|
20
|
-
*
|
|
21
|
-
* Two things the resolver deliberately does NOT do: it never downloads
|
|
22
|
-
* anything, and it never resolves against a repository the project has not
|
|
23
|
-
* added. A dependency on an unknown repository comes back as a `MissingRepo`
|
|
24
|
-
* carrying its provenance, so the trust layer can ask about it by name.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import semver from "semver";
|
|
28
|
-
import { ConfigError } from "../errors.js";
|
|
29
|
-
import type { IndexFile } from "./formats/index-file.js";
|
|
30
|
-
import type { LockKind } from "./formats/lockfile.js";
|
|
31
|
-
import type { RecipeManifest, VariableDefinition } from "./formats/recipe-manifest.js";
|
|
32
|
-
import { indexDependencyLists, type DependencyLists } from "./declarations.js";
|
|
33
|
-
import {
|
|
34
|
-
CatalogMatcher,
|
|
35
|
-
RefSource,
|
|
36
|
-
catalogRepoOfIndex,
|
|
37
|
-
formatRef,
|
|
38
|
-
isNamedRef,
|
|
39
|
-
locationOf,
|
|
40
|
-
namespaceOf,
|
|
41
|
-
namespaceRef,
|
|
42
|
-
rangeOf,
|
|
43
|
-
recipeRef,
|
|
44
|
-
repoOf,
|
|
45
|
-
settleDependency,
|
|
46
|
-
sharedRefResolver,
|
|
47
|
-
shortKey,
|
|
48
|
-
splitRecipeKey,
|
|
49
|
-
withRepoName,
|
|
50
|
-
type NamedRef,
|
|
51
|
-
type RepoRef,
|
|
52
|
-
type SousRef,
|
|
53
|
-
} from "../../services/ref-resolver/index.js";
|
|
54
|
-
import { shortNameFromIdentity } from "./identity.js";
|
|
55
|
-
import type { IndexDependency } from "./formats/index-file.js";
|
|
56
|
-
|
|
57
|
-
/** The `requestedBy` holder meaning "the project asked for this directly". */
|
|
58
|
-
export const PROJECT_REQUESTER = "project";
|
|
59
|
-
|
|
60
|
-
/** One thing to resolve. */
|
|
61
|
-
export type RefRequest = {
|
|
62
|
-
/** The ref, already parsed. */
|
|
63
|
-
ref: NamedRef;
|
|
64
|
-
/**
|
|
65
|
-
* The version range every recipe of a namespace ref must satisfy, and the
|
|
66
|
-
* range of a recipe ref that carries none of its own.
|
|
67
|
-
*/
|
|
68
|
-
range?: string;
|
|
69
|
-
/** Whether prerelease versions may take part in range matching. */
|
|
70
|
-
prerelease?: boolean;
|
|
71
|
-
/**
|
|
72
|
-
* Who asked: the literal "project" for a subscription the project holds
|
|
73
|
-
* directly, or the recipe key of the recipe whose manifest asked.
|
|
74
|
-
*/
|
|
75
|
-
requestedBy: string;
|
|
76
|
-
/**
|
|
77
|
-
* Whether this is a co-subscription or a build dependency. Defaults to
|
|
78
|
-
* "subscribes", which is what a project's own subscriptions are.
|
|
79
|
-
*/
|
|
80
|
-
kind?: LockKind;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
/** What the resolver needs to know about a repository the project has added. */
|
|
84
|
-
export type ResolverRepo = {
|
|
85
|
-
/** Where the repository lives. */
|
|
86
|
-
url: string;
|
|
87
|
-
/**
|
|
88
|
-
* The repository's canonical identity, `<host>/<owner path>/<name>`. A
|
|
89
|
-
* dependency that names another repository names it by location, so this is
|
|
90
|
-
* what decides whether the project has already added it, whatever short name
|
|
91
|
-
* the project gave it.
|
|
92
|
-
*/
|
|
93
|
-
identity: string;
|
|
94
|
-
/** The provider it names, when it names one. */
|
|
95
|
-
provider?: string;
|
|
96
|
-
/** Whether it prefers a newer in-range version over the locked one. */
|
|
97
|
-
alwaysPull?: boolean;
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Loads a resolved recipe's manifest, which is what names its dependencies.
|
|
102
|
-
* Returning undefined means "not available": the recipe's dependencies are then
|
|
103
|
-
* read from its index entry, and when that does not record them either, it is
|
|
104
|
-
* reported under `missingManifests` rather than having its dependencies walked.
|
|
105
|
-
*/
|
|
106
|
-
export type RecipeManifestLoader = (
|
|
107
|
-
recipe: ResolvedRecipe
|
|
108
|
-
) => Promise<RecipeManifest | undefined> | RecipeManifest | undefined;
|
|
109
|
-
|
|
110
|
-
/** Everything the resolver reads. */
|
|
111
|
-
export type ResolveContext = {
|
|
112
|
-
/** The cached index of every added repository, keyed by its short name. */
|
|
113
|
-
indexes: Map<string, IndexFile>;
|
|
114
|
-
/** The added repositories, keyed by short name; the trust list, in other words. */
|
|
115
|
-
repos: Record<string, ResolverRepo>;
|
|
116
|
-
/** How a resolved recipe's manifest is loaded. */
|
|
117
|
-
loadManifest: RecipeManifestLoader;
|
|
118
|
-
/** Whether prereleases are allowed when a request does not say. Defaults to false. */
|
|
119
|
-
prerelease?: boolean;
|
|
120
|
-
/**
|
|
121
|
-
* Versions to hold where they are, keyed by recipe key. A recipe named here
|
|
122
|
-
* resolves to that version whenever it still satisfies every range asked of
|
|
123
|
-
* it, rather than to the newest one that does; a range that no longer allows
|
|
124
|
-
* it wins, and the newest satisfying version is chosen as usual. This is how
|
|
125
|
-
* an update moves only the pins it was asked to move.
|
|
126
|
-
*/
|
|
127
|
-
keep?: Record<string, string>;
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
/** One recipe version the resolver settled on. */
|
|
131
|
-
export type ResolvedRecipe = {
|
|
132
|
-
/** The recipe key, `namespace/recipe`. */
|
|
133
|
-
key: string;
|
|
134
|
-
namespace: string;
|
|
135
|
-
name: string;
|
|
136
|
-
/** The short name of the repository it resolves in, as this project calls it. */
|
|
137
|
-
repo: string;
|
|
138
|
-
/** That repository's canonical identity, which the machine-wide store is keyed by. */
|
|
139
|
-
identity: string;
|
|
140
|
-
/** The exact version chosen. */
|
|
141
|
-
version: string;
|
|
142
|
-
/** The content hash the index publishes for that version. */
|
|
143
|
-
hash: string;
|
|
144
|
-
/** The git tag carrying that version. */
|
|
145
|
-
tag: string;
|
|
146
|
-
/** The recipe folder, relative to the repository root. */
|
|
147
|
-
path: string;
|
|
148
|
-
/** Whether anything holds it as a co-subscription; otherwise a build dependency. */
|
|
149
|
-
kind: LockKind;
|
|
150
|
-
/** Everyone holding it: "project", and the key of every recipe that asked. */
|
|
151
|
-
requestedBy: string[];
|
|
152
|
-
/** Every range that had to be satisfied at once, with who asked for it. */
|
|
153
|
-
ranges: Array<{ range: string; requestedBy: string }>;
|
|
154
|
-
/** Whether prereleases took part in the match. */
|
|
155
|
-
prerelease: boolean;
|
|
156
|
-
/**
|
|
157
|
-
* What the repository's index says this exact version depends on, when the
|
|
158
|
-
* index records it. These are the versions the release resolved, so a
|
|
159
|
-
* dependency of an indexed recipe is installed at the version it was
|
|
160
|
-
* published against rather than at whatever its range would reach today.
|
|
161
|
-
*/
|
|
162
|
-
dependencies?: Record<string, IndexDependency>;
|
|
163
|
-
/**
|
|
164
|
-
* The variable definitions the index records for this exact version, when it
|
|
165
|
-
* records them: the questions it asks, known before its files are fetched.
|
|
166
|
-
*/
|
|
167
|
-
variables?: VariableDefinition[];
|
|
168
|
-
/**
|
|
169
|
-
* The manifest `depends` and `subscribes` lists the index records for this
|
|
170
|
-
* exact version, when it records them, so the recipe can be walked before its
|
|
171
|
-
* files are fetched.
|
|
172
|
-
*/
|
|
173
|
-
declares?: DependencyLists;
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
/** A repository something needs that the project has not added. */
|
|
177
|
-
export type MissingRepo = {
|
|
178
|
-
/**
|
|
179
|
-
* The short name to record it under: the one a ref qualified it with, or one
|
|
180
|
-
* derived from its location when a dependency named it by URL.
|
|
181
|
-
*/
|
|
182
|
-
name: string;
|
|
183
|
-
/** Its URL, which a dependency's locator carries. */
|
|
184
|
-
url?: string;
|
|
185
|
-
/** Its canonical identity, when the dependency named a location. */
|
|
186
|
-
identity?: string;
|
|
187
|
-
/** The provider its locator named, when it named one. */
|
|
188
|
-
provider?: string;
|
|
189
|
-
/** Every ref that needs it, and who asked for that ref. */
|
|
190
|
-
requiredBy: Array<{ ref: string; requestedBy: string }>;
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
/** What a resolution produced. */
|
|
194
|
-
export type ResolveResult = {
|
|
195
|
-
/** Every recipe version to install, in a stable order (by key). */
|
|
196
|
-
resolved: ResolvedRecipe[];
|
|
197
|
-
/** Repositories a dependency needs that the project has not added. */
|
|
198
|
-
missingRepos: MissingRepo[];
|
|
199
|
-
/**
|
|
200
|
-
* Resolved recipes whose dependencies could not be read: the loader produced
|
|
201
|
-
* no manifest, and the index entry does not record them.
|
|
202
|
-
*/
|
|
203
|
-
missingManifests: string[];
|
|
204
|
-
/** Any dependency cycle found, as the chain of recipe keys forming it. */
|
|
205
|
-
cycles: string[][];
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
/** A pending piece of work: one ref to resolve on behalf of one holder. */
|
|
209
|
-
type WorkItem = RefRequest & {
|
|
210
|
-
kind: LockKind;
|
|
211
|
-
/**
|
|
212
|
-
* Where the ref said the recipe lives, when a manifest named another
|
|
213
|
-
* repository by location. The project may already have added that repository
|
|
214
|
-
* under any short name at all, so it is matched by identity.
|
|
215
|
-
*/
|
|
216
|
-
remote?: {
|
|
217
|
-
/** The repository's canonical identity. */
|
|
218
|
-
identity: string;
|
|
219
|
-
/** Its HTTPS location, which is what adding it would be handed. */
|
|
220
|
-
url: string;
|
|
221
|
-
/** The provider the locator named. */
|
|
222
|
-
provider?: string;
|
|
223
|
-
/** The dependency exactly as the manifest wrote it, for messages. */
|
|
224
|
-
written: string;
|
|
225
|
-
/**
|
|
226
|
-
* The repository ref a browser path dependency was read as (its location,
|
|
227
|
-
* its path and the range the manifest wrote), settled through the
|
|
228
|
-
* repository's index once the project has it. Until then `ref` holds no
|
|
229
|
-
* namespace.
|
|
230
|
-
*/
|
|
231
|
-
browsed?: RepoRef;
|
|
232
|
-
/** What the declaring version's index records, for pins once a browsed path is settled. */
|
|
233
|
-
pins?: Record<string, IndexDependency>;
|
|
234
|
-
};
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Turns one entry of a manifest's `depends` or `subscribes` into a piece of
|
|
239
|
-
* work.
|
|
240
|
-
*
|
|
241
|
-
* Two things are decided here. A SIBLING ref resolves inside the declaring
|
|
242
|
-
* recipe's own repository, never across the others, because that is what
|
|
243
|
-
* writing it without a location means. And when the repository's index records
|
|
244
|
-
* what this exact version of the parent was released against, that exact
|
|
245
|
-
* version is what gets asked for, rather than whatever the declared range would
|
|
246
|
-
* reach today.
|
|
247
|
-
*
|
|
248
|
-
* A dependency that reads more than one way (a GitLab URL with nested groups)
|
|
249
|
-
* is settled by what the release recorded in the index, and failing that by
|
|
250
|
-
* what the project's own cached indexes already publish; nothing is fetched to
|
|
251
|
-
* find out. One that still reads several ways is an error naming them.
|
|
252
|
-
*
|
|
253
|
-
* @param written - The dependency as the manifest wrote it.
|
|
254
|
-
* @param parent - The recipe whose manifest declared it.
|
|
255
|
-
* @param kind - Whether it was declared as a dependency or a co-subscription.
|
|
256
|
-
* @param context - The added repositories and their cached indexes.
|
|
257
|
-
*/
|
|
258
|
-
function dependencyRequest(
|
|
259
|
-
written: string,
|
|
260
|
-
parent: ResolvedRecipe,
|
|
261
|
-
kind: LockKind,
|
|
262
|
-
context: ResolveContext
|
|
263
|
-
): WorkItem {
|
|
264
|
-
const reading = settleDependency(written, {
|
|
265
|
-
...(parent.dependencies === undefined ? {} : { recorded: parent.dependencies }),
|
|
266
|
-
known: (candidate) => knownLocally(candidate, context),
|
|
267
|
-
});
|
|
268
|
-
if (reading === undefined) throw unsettledDependencyError(written, parent);
|
|
269
|
-
|
|
270
|
-
const base = {
|
|
271
|
-
requestedBy: parent.key,
|
|
272
|
-
kind,
|
|
273
|
-
...(parent.prerelease ? { prerelease: true } : {}),
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
if (reading.kind === "repo" && reading.browsed !== undefined) {
|
|
277
|
-
return {
|
|
278
|
-
ref: namespaceRef(""),
|
|
279
|
-
...base,
|
|
280
|
-
remote: {
|
|
281
|
-
...remoteOf(reading, written),
|
|
282
|
-
browsed: reading,
|
|
283
|
-
...(parent.dependencies === undefined ? {} : { pins: parent.dependencies }),
|
|
284
|
-
},
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
/* c8 ignore next */
|
|
288
|
-
if (!isNamedRef(reading)) throw unsettledDependencyError(written, parent);
|
|
289
|
-
|
|
290
|
-
const pinned = parent.dependencies?.[shortKey(reading)];
|
|
291
|
-
const ref = pinnedRef(reading, pinned);
|
|
292
|
-
|
|
293
|
-
if (locationOf(reading) === undefined) {
|
|
294
|
-
return { ref: withRepoName(ref, parent.repo), ...base };
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const remote = remoteOf(reading, written);
|
|
298
|
-
return {
|
|
299
|
-
ref,
|
|
300
|
-
...base,
|
|
301
|
-
remote: { ...remote, identity: pinned?.repo ?? remote.identity },
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* A named reading as a request asks for it: at the exact version the index
|
|
307
|
-
* recorded when it recorded one, otherwise at the range the manifest wrote.
|
|
308
|
-
*
|
|
309
|
-
* @param reading - The reading.
|
|
310
|
-
* @param pinned - What the declaring version's index recorded for it.
|
|
311
|
-
*/
|
|
312
|
-
function pinnedRef(reading: NamedRef, pinned: IndexDependency | undefined): NamedRef {
|
|
313
|
-
const namespace = namespaceOf(reading)!.name;
|
|
314
|
-
if (reading.kind === "namespace") return namespaceRef(namespace);
|
|
315
|
-
const range = pinned?.version ?? reading.range ?? pinned?.range;
|
|
316
|
-
return recipeRef(namespace, reading.name, range === undefined ? {} : { range });
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/** Where a located reading's repository lives, as a request carries it. */
|
|
320
|
-
function remoteOf(
|
|
321
|
-
reading: SousRef,
|
|
322
|
-
written: string
|
|
323
|
-
): NonNullable<WorkItem["remote"]> {
|
|
324
|
-
const location = locationOf(reading)!;
|
|
325
|
-
return {
|
|
326
|
-
identity: location.identity,
|
|
327
|
-
url: location.url,
|
|
328
|
-
provider: location.provider,
|
|
329
|
-
written: written.trim(),
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Whether a reading's repository is one the project added and its cached index
|
|
335
|
-
* publishes what the reading names. It reads only what is already here.
|
|
336
|
-
*
|
|
337
|
-
* @param reading - One reading of a dependency.
|
|
338
|
-
* @param context - The added repositories and their cached indexes.
|
|
339
|
-
*/
|
|
340
|
-
function knownLocally(reading: SousRef, context: ResolveContext): boolean {
|
|
341
|
-
const location = locationOf(reading);
|
|
342
|
-
if (location === undefined) return false;
|
|
343
|
-
const added = repoNamedByIdentity(context.repos, location.identity);
|
|
344
|
-
const index = added === undefined ? undefined : context.indexes.get(added);
|
|
345
|
-
if (index === undefined) return false;
|
|
346
|
-
return new CatalogMatcher([catalogRepoOfIndex(index, { location })]).match(reading).length > 0;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* The error for a dependency that reads more than one way with nothing to
|
|
351
|
-
* settle it.
|
|
352
|
-
*
|
|
353
|
-
* @param written - The dependency as written.
|
|
354
|
-
* @param parent - The recipe that declared it.
|
|
355
|
-
*/
|
|
356
|
-
function unsettledDependencyError(written: string, parent: ResolvedRecipe): ConfigError {
|
|
357
|
-
const { refs } = sharedRefResolver().parse(written, RefSource.Manifest);
|
|
358
|
-
return new ConfigError(
|
|
359
|
-
`The dependency '${written.trim()}' of the recipe '${parent.key}' can be read ` +
|
|
360
|
-
`${refs.length} ways, and nothing says which one it means:\n` +
|
|
361
|
-
refs.map((reading) => ` ${formatRef(reading)}`).join("\n") +
|
|
362
|
-
`\n A release records which one it settled on in the index; the index of ` +
|
|
363
|
-
`'${parent.repo}' records none for this version. Release the recipe again with this ` +
|
|
364
|
-
`version of sous, or write the dependency so it reads one way, as one of the forms above.`
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* Settles a browsed path, once the project has the repository it names.
|
|
370
|
-
*
|
|
371
|
-
* @param item - The request, whose `ref` is filled in.
|
|
372
|
-
* @param index - The repository's cached index.
|
|
373
|
-
*/
|
|
374
|
-
function settleBrowsedItem(item: WorkItem, index: IndexFile | undefined): void {
|
|
375
|
-
const remote = item.remote!;
|
|
376
|
-
const browsed = remote.browsed!;
|
|
377
|
-
const settled =
|
|
378
|
-
index === undefined
|
|
379
|
-
? []
|
|
380
|
-
: new CatalogMatcher([catalogRepoOfIndex(index, { location: browsed.location })])
|
|
381
|
-
.match(browsed)
|
|
382
|
-
.map((match) => match.ref)
|
|
383
|
-
.filter(isNamedRef);
|
|
384
|
-
|
|
385
|
-
let found: NamedRef | undefined = settled.length === 1 ? settled[0] : undefined;
|
|
386
|
-
if (found === undefined) {
|
|
387
|
-
// The folder may have moved since the release; the key it settled on then
|
|
388
|
-
// is recorded beside the repository it lives in.
|
|
389
|
-
const recorded = Object.keys(remote.pins ?? {}).filter(
|
|
390
|
-
(key) => remote.pins![key]!.repo === remote.identity
|
|
391
|
-
);
|
|
392
|
-
if (recorded.length === 1) {
|
|
393
|
-
const { namespace, name } = splitRecipeKey(recorded[0]!);
|
|
394
|
-
found = recipeRef(namespace, name);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (found === undefined) {
|
|
399
|
-
throw new ConfigError(
|
|
400
|
-
`The dependency '${remote.written}' names a folder, and the index of the repository at ` +
|
|
401
|
-
`${remote.url} ${settled.length === 0 ? "publishes no recipe there" : "publishes several recipes there"}.` +
|
|
402
|
-
`\n It was required by the recipe '${item.requestedBy}'. Write the dependency as the ` +
|
|
403
|
-
`recipe it means, such as '${remote.url}/<namespace>/<recipe>'.`
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const withRange =
|
|
408
|
-
found.kind === "recipe" && browsed.range !== undefined ? { ...found, range: browsed.range } : found;
|
|
409
|
-
item.ref = withRepoName(pinnedRef(withRange, remote.pins?.[shortKey(found)]), repoOf(item.ref)?.name);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* The short name of the added repository with this identity, or undefined when
|
|
414
|
-
* the project has added none. Short names are a project's own labels, so the
|
|
415
|
-
* identity is what a dependency is matched on.
|
|
416
|
-
*
|
|
417
|
-
* @param repos - The repositories the project has added.
|
|
418
|
-
* @param identity - The canonical identity to look for.
|
|
419
|
-
*/
|
|
420
|
-
export function repoNamedByIdentity(
|
|
421
|
-
repos: Record<string, ResolverRepo>,
|
|
422
|
-
identity: string
|
|
423
|
-
): string | undefined {
|
|
424
|
-
for (const [name, entry] of Object.entries(repos)) {
|
|
425
|
-
if (entry.identity === identity) return name;
|
|
426
|
-
}
|
|
427
|
-
return undefined;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* A short name for a repository the project has not added yet, derived from its
|
|
432
|
-
* location and made unique against the names already in use.
|
|
433
|
-
*
|
|
434
|
-
* @param repos - The repositories the project has added.
|
|
435
|
-
* @param identity - The canonical identity of the repository being named.
|
|
436
|
-
*/
|
|
437
|
-
export function proposeRepoName(
|
|
438
|
-
repos: Record<string, ResolverRepo>,
|
|
439
|
-
identity: string
|
|
440
|
-
): string {
|
|
441
|
-
const base = shortNameFromIdentity(identity);
|
|
442
|
-
if (!Object.hasOwn(repos, base)) return base;
|
|
443
|
-
for (let suffix = 2; ; suffix++) {
|
|
444
|
-
const candidate = `${base}-${suffix}`;
|
|
445
|
-
if (!Object.hasOwn(repos, candidate)) return candidate;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
/**
|
|
450
|
-
* Resolves a set of refs and the whole dependency closure beneath them.
|
|
451
|
-
*
|
|
452
|
-
* @param requests - What the project (or the command line) asked for.
|
|
453
|
-
* @param context - The cached indexes, the added repositories and a manifest loader.
|
|
454
|
-
*/
|
|
455
|
-
export async function resolveRefs(
|
|
456
|
-
requests: RefRequest[],
|
|
457
|
-
context: ResolveContext
|
|
458
|
-
): Promise<ResolveResult> {
|
|
459
|
-
const resolved = new Map<string, ResolvedRecipe>();
|
|
460
|
-
const missingRepos = new Map<string, MissingRepo>();
|
|
461
|
-
const missingManifests = new Set<string>();
|
|
462
|
-
const cycles: string[][] = [];
|
|
463
|
-
/** Which recipe pulled each recipe in, used to find a cycle's chain. */
|
|
464
|
-
const parents = new Map<string, string>();
|
|
465
|
-
/** Recipes whose manifest has already been walked, at the version noted. */
|
|
466
|
-
const walked = new Map<string, string>();
|
|
467
|
-
|
|
468
|
-
const queue: WorkItem[] = requests.map((request) => ({
|
|
469
|
-
...request,
|
|
470
|
-
// A recipe ref that carries no range of its own takes the request's.
|
|
471
|
-
ref:
|
|
472
|
-
request.ref.kind === "recipe" && request.ref.range === undefined && request.range !== undefined
|
|
473
|
-
? { ...request.ref, range: request.range }
|
|
474
|
-
: request.ref,
|
|
475
|
-
kind: request.kind ?? "subscribes",
|
|
476
|
-
}));
|
|
477
|
-
|
|
478
|
-
while (queue.length > 0) {
|
|
479
|
-
const item = queue.shift()!;
|
|
480
|
-
|
|
481
|
-
// A ref naming a repository the project has not added never resolves and
|
|
482
|
-
// never downloads anything; it comes back as a missing repository instead,
|
|
483
|
-
// so the trust layer can ask about it by name.
|
|
484
|
-
const qualifier = repoOf(item.ref)?.name;
|
|
485
|
-
if (qualifier !== undefined && !Object.hasOwn(context.repos, qualifier)) {
|
|
486
|
-
recordMissingRepo(missingRepos, {
|
|
487
|
-
name: qualifier,
|
|
488
|
-
requiredBy: [{ ref: formatRef(item.ref), requestedBy: item.requestedBy }],
|
|
489
|
-
});
|
|
490
|
-
continue;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// A dependency that named another repository by location is matched on that
|
|
494
|
-
// location, so a project that already added it under some other short name
|
|
495
|
-
// resolves against the copy it has. One it has not added carries its URL and
|
|
496
|
-
// its provider, which is everything the trust round needs to offer to add it.
|
|
497
|
-
if (item.remote !== undefined) {
|
|
498
|
-
const added = repoNamedByIdentity(context.repos, item.remote.identity);
|
|
499
|
-
if (added === undefined) {
|
|
500
|
-
recordMissingRepo(missingRepos, {
|
|
501
|
-
name: proposeRepoName(context.repos, item.remote.identity),
|
|
502
|
-
url: item.remote.url,
|
|
503
|
-
identity: item.remote.identity,
|
|
504
|
-
...(item.remote.provider === undefined ? {} : { provider: item.remote.provider }),
|
|
505
|
-
requiredBy: [{ ref: item.remote.written, requestedBy: item.requestedBy }],
|
|
506
|
-
});
|
|
507
|
-
continue;
|
|
508
|
-
}
|
|
509
|
-
item.ref = withRepoName(item.ref, added);
|
|
510
|
-
if (item.remote.browsed !== undefined) settleBrowsedItem(item, context.indexes.get(added));
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
if (item.ref.kind === "namespace") {
|
|
514
|
-
queue.push(...expandNamespace(item, context));
|
|
515
|
-
continue;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
const recipe = resolveRecipeRef(item, context, resolved);
|
|
519
|
-
resolved.set(recipe.key, recipe);
|
|
520
|
-
|
|
521
|
-
if (item.requestedBy !== PROJECT_REQUESTER && !parents.has(recipe.key)) {
|
|
522
|
-
parents.set(recipe.key, item.requestedBy);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// Walking a manifest is only worth doing once per version. A second holder
|
|
526
|
-
// of an already-walked version adds itself and stops there; that is also
|
|
527
|
-
// what keeps a dependency cycle finite.
|
|
528
|
-
if (walked.get(recipe.key) === recipe.version) {
|
|
529
|
-
const cycle = findCycle(recipe.key, item.requestedBy, parents);
|
|
530
|
-
if (cycle !== undefined) cycles.push(cycle);
|
|
531
|
-
continue;
|
|
532
|
-
}
|
|
533
|
-
walked.set(recipe.key, recipe.version);
|
|
534
|
-
|
|
535
|
-
const lists = dependencyListsOf(recipe, await context.loadManifest(recipe));
|
|
536
|
-
if (lists === undefined) {
|
|
537
|
-
missingManifests.add(recipe.key);
|
|
538
|
-
continue;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
for (const [kind, refs] of [
|
|
542
|
-
["depends", lists.depends],
|
|
543
|
-
["subscribes", lists.subscribes],
|
|
544
|
-
] as Array<[LockKind, string[]]>) {
|
|
545
|
-
for (const written of refs) {
|
|
546
|
-
queue.push(dependencyRequest(written, recipe, kind, context));
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// A recipe whose version was narrowed by a later holder has had TWO of its
|
|
552
|
-
// versions walked, and the dependencies discovered from the version that was
|
|
553
|
-
// replaced are still sitting in `resolved`. Walk the closure once more over
|
|
554
|
-
// the versions actually settled on, and keep only what that reaches.
|
|
555
|
-
await keepOnlyReachable(resolved, context);
|
|
556
|
-
|
|
557
|
-
const ordered = [...resolved.values()].sort((left, right) =>
|
|
558
|
-
left.key < right.key ? -1 : left.key > right.key ? 1 : 0
|
|
559
|
-
);
|
|
560
|
-
for (const recipe of ordered) recipe.requestedBy.sort();
|
|
561
|
-
|
|
562
|
-
return {
|
|
563
|
-
resolved: ordered,
|
|
564
|
-
missingRepos: [...missingRepos.values()].sort((left, right) =>
|
|
565
|
-
left.name < right.name ? -1 : 1
|
|
566
|
-
),
|
|
567
|
-
missingManifests: [...missingManifests].sort(),
|
|
568
|
-
cycles,
|
|
569
|
-
};
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
/**
|
|
573
|
-
* Drops every resolved recipe the settled closure no longer reaches, and trims
|
|
574
|
-
* the holders and ranges of the ones that stay.
|
|
575
|
-
*
|
|
576
|
-
* The walk resolves refs in the order it meets them, so a recipe can be walked
|
|
577
|
-
* at one version and then walked again at a lower one once a second holder
|
|
578
|
-
* narrows its range. The lower version is what the closure settles on, but the
|
|
579
|
-
* dependencies discovered from the higher one are already in `resolved`, held by
|
|
580
|
-
* a parent that no longer declares them. Installing those is wrong twice over:
|
|
581
|
-
* they are content nothing asked for, and the lockfile would record a holder
|
|
582
|
-
* that does not hold them.
|
|
583
|
-
*
|
|
584
|
-
* So the closure is walked once more over the versions actually settled on. This
|
|
585
|
-
* re-reads manifests that have already been read, which the loader serves from
|
|
586
|
-
* the store; nothing is fetched. A recipe the loader cannot produce a manifest
|
|
587
|
-
* for keeps everything it reached, since the alternative is dropping a
|
|
588
|
-
* dependency because a manifest was unreadable.
|
|
589
|
-
*
|
|
590
|
-
* Versions are NOT re-picked. Trimming can only remove constraints, so the
|
|
591
|
-
* version already chosen still satisfies every holder that remains; re-picking
|
|
592
|
-
* could raise it and undo the narrowing that made this pass necessary.
|
|
593
|
-
*
|
|
594
|
-
* @param resolved - What the walk produced, edited in place.
|
|
595
|
-
* @param context - The manifest loader.
|
|
596
|
-
*/
|
|
597
|
-
async function keepOnlyReachable(
|
|
598
|
-
resolved: Map<string, ResolvedRecipe>,
|
|
599
|
-
context: ResolveContext
|
|
600
|
-
): Promise<void> {
|
|
601
|
-
/** Who declares each key, and under which kind, in the settled closure. */
|
|
602
|
-
const holders = new Map<string, Map<string, LockKind>>();
|
|
603
|
-
|
|
604
|
-
/** Records one declaration, and reports whether the child is newly reached. */
|
|
605
|
-
const declare = (parent: string, child: string, kind: LockKind): boolean => {
|
|
606
|
-
const existing = holders.get(child);
|
|
607
|
-
if (existing === undefined) {
|
|
608
|
-
holders.set(child, new Map([[parent, kind]]));
|
|
609
|
-
return true;
|
|
610
|
-
}
|
|
611
|
-
const previous = existing.get(parent);
|
|
612
|
-
existing.set(parent, previous === "subscribes" || kind === "subscribes" ? "subscribes" : kind);
|
|
613
|
-
return false;
|
|
614
|
-
};
|
|
615
|
-
|
|
616
|
-
const queue: string[] = [];
|
|
617
|
-
for (const recipe of resolved.values()) {
|
|
618
|
-
if (!recipe.requestedBy.includes(PROJECT_REQUESTER)) continue;
|
|
619
|
-
if (declare(PROJECT_REQUESTER, recipe.key, "subscribes")) queue.push(recipe.key);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
while (queue.length > 0) {
|
|
623
|
-
const key = queue.shift()!;
|
|
624
|
-
const recipe = resolved.get(key);
|
|
625
|
-
if (recipe === undefined) continue;
|
|
626
|
-
|
|
627
|
-
let manifest;
|
|
628
|
-
try {
|
|
629
|
-
manifest = await context.loadManifest(recipe);
|
|
630
|
-
} catch {
|
|
631
|
-
manifest = undefined;
|
|
632
|
-
}
|
|
633
|
-
const lists = dependencyListsOf(recipe, manifest);
|
|
634
|
-
|
|
635
|
-
if (lists === undefined) {
|
|
636
|
-
// An unreadable manifest is already reported as a missing manifest. Keep
|
|
637
|
-
// everything this recipe held rather than dropping a dependency over it.
|
|
638
|
-
for (const other of resolved.values()) {
|
|
639
|
-
if (!other.requestedBy.includes(key)) continue;
|
|
640
|
-
const kind = other.kind;
|
|
641
|
-
if (declare(key, other.key, kind)) queue.push(other.key);
|
|
642
|
-
}
|
|
643
|
-
continue;
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
for (const [kind, refs] of [
|
|
647
|
-
["depends", lists.depends],
|
|
648
|
-
["subscribes", lists.subscribes],
|
|
649
|
-
] as Array<[LockKind, string[]]>) {
|
|
650
|
-
for (const written of refs) {
|
|
651
|
-
for (const target of dependencyTargets(written, recipe, context, resolved)) {
|
|
652
|
-
if (!resolved.has(target)) continue;
|
|
653
|
-
if (declare(key, target, kind)) queue.push(target);
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
for (const [key, recipe] of [...resolved]) {
|
|
660
|
-
const reached = holders.get(key);
|
|
661
|
-
if (reached === undefined) {
|
|
662
|
-
resolved.delete(key);
|
|
663
|
-
continue;
|
|
664
|
-
}
|
|
665
|
-
recipe.requestedBy = [...reached.keys()];
|
|
666
|
-
recipe.ranges = recipe.ranges.filter((entry) => reached.has(entry.requestedBy));
|
|
667
|
-
recipe.kind = [...reached.values()].includes("subscribes") ? "subscribes" : "depends";
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
/**
|
|
672
|
-
* The resolved keys one dependency of a recipe reaches, the same way the walk
|
|
673
|
-
* reached them: a namespace means every recipe in it, and a browsed path means
|
|
674
|
-
* the recipe it settles on. A dependency that no longer settles reaches
|
|
675
|
-
* nothing, because the walk would have failed on it.
|
|
676
|
-
*
|
|
677
|
-
* @param written - The dependency as the manifest wrote it.
|
|
678
|
-
* @param holder - The recipe that declared it.
|
|
679
|
-
* @param context - The added repositories and their cached indexes.
|
|
680
|
-
* @param resolved - What the walk resolved.
|
|
681
|
-
*/
|
|
682
|
-
function dependencyTargets(
|
|
683
|
-
written: string,
|
|
684
|
-
holder: ResolvedRecipe,
|
|
685
|
-
context: ResolveContext,
|
|
686
|
-
resolved: Map<string, ResolvedRecipe>
|
|
687
|
-
): string[] {
|
|
688
|
-
let item: WorkItem;
|
|
689
|
-
try {
|
|
690
|
-
item = dependencyRequest(written, holder, "depends", context);
|
|
691
|
-
if (item.remote?.browsed !== undefined) {
|
|
692
|
-
const added = repoNamedByIdentity(context.repos, item.remote.identity);
|
|
693
|
-
settleBrowsedItem(item, added === undefined ? undefined : context.indexes.get(added));
|
|
694
|
-
}
|
|
695
|
-
} catch {
|
|
696
|
-
return [];
|
|
697
|
-
}
|
|
698
|
-
const namespace = namespaceOf(item.ref)!.name;
|
|
699
|
-
return item.ref.kind === "namespace"
|
|
700
|
-
? [...resolved.keys()].filter((entry) => entry.startsWith(`${namespace}/`))
|
|
701
|
-
: [shortKey(item.ref)];
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
/**
|
|
705
|
-
* What a resolved recipe declares under `depends` and `subscribes`: its
|
|
706
|
-
* manifest's lists when the manifest could be read, and otherwise the lists its
|
|
707
|
-
* index entry records, or undefined when neither says.
|
|
708
|
-
*
|
|
709
|
-
* @param recipe - The resolved recipe, carrying the lists its index entry records.
|
|
710
|
-
* @param manifest - Its manifest, when the loader produced one.
|
|
711
|
-
*/
|
|
712
|
-
function dependencyListsOf(
|
|
713
|
-
recipe: ResolvedRecipe,
|
|
714
|
-
manifest: RecipeManifest | undefined
|
|
715
|
-
): DependencyLists | undefined {
|
|
716
|
-
if (manifest !== undefined) {
|
|
717
|
-
return { depends: manifest.depends ?? [], subscribes: manifest.subscribes ?? [] };
|
|
718
|
-
}
|
|
719
|
-
return recipe.declares;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
/** Records one more reason a repository is needed. */
|
|
723
|
-
function recordMissingRepo(into: Map<string, MissingRepo>, missing: MissingRepo): void {
|
|
724
|
-
const existing = into.get(missing.name);
|
|
725
|
-
if (existing === undefined) {
|
|
726
|
-
into.set(missing.name, missing);
|
|
727
|
-
return;
|
|
728
|
-
}
|
|
729
|
-
for (const entry of missing.requiredBy) {
|
|
730
|
-
const duplicate = existing.requiredBy.some(
|
|
731
|
-
(known) => known.ref === entry.ref && known.requestedBy === entry.requestedBy
|
|
732
|
-
);
|
|
733
|
-
if (!duplicate) existing.requiredBy.push(entry);
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
/**
|
|
738
|
-
* Expands a namespace ref into one request per recipe in that namespace. A
|
|
739
|
-
* namespace subscription means "everything in here, including whatever is added
|
|
740
|
-
* later", so the expansion happens fresh on every resolution.
|
|
741
|
-
*
|
|
742
|
-
* @param item - The namespace request.
|
|
743
|
-
* @param context - The cached indexes and added repositories.
|
|
744
|
-
*/
|
|
745
|
-
function expandNamespace(item: WorkItem, context: ResolveContext): WorkItem[] {
|
|
746
|
-
const namespace = namespaceOf(item.ref)!.name;
|
|
747
|
-
const qualifier = repoOf(item.ref)?.name;
|
|
748
|
-
const requestedRange = item.range;
|
|
749
|
-
const expanded: WorkItem[] = [];
|
|
750
|
-
const seen = new Set<string>();
|
|
751
|
-
let namespaceFound = false;
|
|
752
|
-
|
|
753
|
-
for (const [repoName, index] of context.indexes) {
|
|
754
|
-
if (qualifier !== undefined && repoName !== qualifier) continue;
|
|
755
|
-
if (!Object.hasOwn(index.namespaces, namespace)) continue;
|
|
756
|
-
namespaceFound = true;
|
|
757
|
-
|
|
758
|
-
for (const key of Object.keys(index.recipes)) {
|
|
759
|
-
if (!key.startsWith(`${namespace}/`)) continue;
|
|
760
|
-
if (seen.has(key)) continue;
|
|
761
|
-
seen.add(key);
|
|
762
|
-
expanded.push({
|
|
763
|
-
// A namespace ref cannot carry a range when it is WRITTEN, but a
|
|
764
|
-
// namespace subscription entry can carry one, and a caller builds the
|
|
765
|
-
// request from that entry. When it does, the range applies to every
|
|
766
|
-
// recipe in the namespace; that is how the built-in `core`
|
|
767
|
-
// subscription stays pinned to the running sous version.
|
|
768
|
-
ref: recipeRef(namespace, key.slice(namespace.length + 1), {
|
|
769
|
-
...(qualifier === undefined ? {} : { repo: qualifier }),
|
|
770
|
-
...(requestedRange === undefined ? {} : { range: requestedRange }),
|
|
771
|
-
}),
|
|
772
|
-
requestedBy: item.requestedBy,
|
|
773
|
-
kind: item.kind,
|
|
774
|
-
...(item.prerelease === undefined ? {} : { prerelease: item.prerelease }),
|
|
775
|
-
});
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
if (!namespaceFound) throw unknownRefError(item, context, "namespace");
|
|
780
|
-
|
|
781
|
-
return expanded;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
/**
|
|
785
|
-
* Resolves one recipe ref against every added repository, merging it with
|
|
786
|
-
* anything already resolved for the same recipe key.
|
|
787
|
-
*
|
|
788
|
-
* @param item - The request being resolved.
|
|
789
|
-
* @param context - The cached indexes and added repositories.
|
|
790
|
-
* @param resolved - What is resolved so far, so ranges accumulate.
|
|
791
|
-
*/
|
|
792
|
-
function resolveRecipeRef(
|
|
793
|
-
item: WorkItem,
|
|
794
|
-
context: ResolveContext,
|
|
795
|
-
resolved: Map<string, ResolvedRecipe>
|
|
796
|
-
): ResolvedRecipe {
|
|
797
|
-
const key = shortKey(item.ref);
|
|
798
|
-
const qualifier = repoOf(item.ref)?.name;
|
|
799
|
-
|
|
800
|
-
const candidates: Array<{ repo: string; index: IndexFile }> = [];
|
|
801
|
-
for (const [repoName, index] of context.indexes) {
|
|
802
|
-
if (qualifier !== undefined && repoName !== qualifier) continue;
|
|
803
|
-
if (Object.hasOwn(index.recipes, key)) candidates.push({ repo: repoName, index });
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
if (candidates.length === 0) throw unknownRefError(item, context, "recipe");
|
|
807
|
-
if (candidates.length > 1) throw ambiguousRefError(item, candidates.map((c) => c.repo));
|
|
808
|
-
|
|
809
|
-
const chosen = candidates[0]!;
|
|
810
|
-
const entry = chosen.index.recipes[key]!;
|
|
811
|
-
const previous = resolved.get(key);
|
|
812
|
-
|
|
813
|
-
if (previous !== undefined && previous.repo !== chosen.repo) {
|
|
814
|
-
throw new ConfigError(
|
|
815
|
-
`The recipe '${key}' is being taken from two different repositories at once: ` +
|
|
816
|
-
`'${previous.repo}' and '${chosen.repo}'.\n` +
|
|
817
|
-
` Qualify the refs that ask for it, so each one says which repository it means.`
|
|
818
|
-
);
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
const ranges = [...(previous?.ranges ?? [])];
|
|
822
|
-
const range = rangeOf(item.ref) ?? "*";
|
|
823
|
-
if (!ranges.some((known) => known.range === range && known.requestedBy === item.requestedBy)) {
|
|
824
|
-
ranges.push({ range, requestedBy: item.requestedBy });
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
const prerelease =
|
|
828
|
-
(previous?.prerelease ?? false) ||
|
|
829
|
-
(item.prerelease ?? context.prerelease ?? false);
|
|
830
|
-
|
|
831
|
-
const version = pickVersion(key, entry, ranges, prerelease, context.keep?.[key]);
|
|
832
|
-
const versionEntry = entry.versions[version]!;
|
|
833
|
-
const declares = indexDependencyLists(versionEntry);
|
|
834
|
-
|
|
835
|
-
const requestedBy = [...(previous?.requestedBy ?? [])];
|
|
836
|
-
if (!requestedBy.includes(item.requestedBy)) requestedBy.push(item.requestedBy);
|
|
837
|
-
|
|
838
|
-
const { namespace, name } = splitRecipeKey(key);
|
|
839
|
-
return {
|
|
840
|
-
key,
|
|
841
|
-
namespace,
|
|
842
|
-
name,
|
|
843
|
-
repo: chosen.repo,
|
|
844
|
-
identity: context.repos[chosen.repo]?.identity ?? chosen.repo,
|
|
845
|
-
version,
|
|
846
|
-
hash: versionEntry.hash,
|
|
847
|
-
tag: versionEntry.tag,
|
|
848
|
-
path: entry.path,
|
|
849
|
-
...(versionEntry.dependencies === undefined
|
|
850
|
-
? {}
|
|
851
|
-
: { dependencies: versionEntry.dependencies }),
|
|
852
|
-
...(versionEntry.variables === undefined ? {} : { variables: versionEntry.variables }),
|
|
853
|
-
...(declares === undefined ? {} : { declares }),
|
|
854
|
-
// A recipe held as a co-subscription by anyone is a co-subscription; a
|
|
855
|
-
// build dependency only stays one while nothing subscribes to it.
|
|
856
|
-
kind: previous?.kind === "subscribes" || item.kind === "subscribes" ? "subscribes" : "depends",
|
|
857
|
-
requestedBy,
|
|
858
|
-
ranges,
|
|
859
|
-
prerelease,
|
|
860
|
-
};
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
/**
|
|
864
|
-
* Picks the highest published version satisfying every range at once.
|
|
865
|
-
* Prereleases stay out of the match unless they were opted into.
|
|
866
|
-
*
|
|
867
|
-
* @param key - The recipe key, for error messages.
|
|
868
|
-
* @param entry - The recipe's index entry.
|
|
869
|
-
* @param ranges - Every range that has to hold, with who asked for it.
|
|
870
|
-
* @param prerelease - Whether prereleases may match.
|
|
871
|
-
* @param keep - A version to hold, chosen whenever it is among the candidates.
|
|
872
|
-
*/
|
|
873
|
-
function pickVersion(
|
|
874
|
-
key: string,
|
|
875
|
-
entry: IndexFile["recipes"][string],
|
|
876
|
-
ranges: Array<{ range: string; requestedBy: string }>,
|
|
877
|
-
prerelease: boolean,
|
|
878
|
-
keep?: string
|
|
879
|
-
): string {
|
|
880
|
-
const published = Object.keys(entry.versions);
|
|
881
|
-
const eligible = prerelease
|
|
882
|
-
? published
|
|
883
|
-
: published.filter((version) => entry.versions[version]!.prerelease !== true);
|
|
884
|
-
|
|
885
|
-
let candidates = eligible;
|
|
886
|
-
for (const { range } of ranges) {
|
|
887
|
-
candidates = candidates.filter((version) =>
|
|
888
|
-
semver.satisfies(version, range, { includePrerelease: prerelease })
|
|
889
|
-
);
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
// A held version is kept exactly as long as every range still allows it. It
|
|
893
|
-
// is checked against the published list rather than the eligible one, so a
|
|
894
|
-
// prerelease a subscription once opted into is not moved just for being one.
|
|
895
|
-
if (
|
|
896
|
-
keep !== undefined &&
|
|
897
|
-
Object.hasOwn(entry.versions, keep) &&
|
|
898
|
-
ranges.every(({ range }) =>
|
|
899
|
-
semver.satisfies(keep, range, { includePrerelease: true })
|
|
900
|
-
)
|
|
901
|
-
) {
|
|
902
|
-
return keep;
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
const best = semver.maxSatisfying(candidates, "*", { includePrerelease: prerelease });
|
|
906
|
-
if (best !== null) return best;
|
|
907
|
-
|
|
908
|
-
const asked = ranges
|
|
909
|
-
.map(({ range, requestedBy }) => ` ${range} (required by ${requestedBy})`)
|
|
910
|
-
.join("\n");
|
|
911
|
-
const available = sortVersions(published).join(", ");
|
|
912
|
-
const prereleaseNote = prerelease
|
|
913
|
-
? ""
|
|
914
|
-
: "\n Prerelease versions were not considered. A subscription may opt into them " +
|
|
915
|
-
"with 'prerelease: true', and 'sous subscribe' with '--prerelease'.";
|
|
916
|
-
|
|
917
|
-
throw new ConfigError(
|
|
918
|
-
`No published version of '${key}' satisfies what was asked for.\n` +
|
|
919
|
-
` Version ${ranges.length === 1 ? "range" : "ranges"} asked for:\n${asked}\n` +
|
|
920
|
-
` Versions this repository publishes: ${available}.${prereleaseNote}`
|
|
921
|
-
);
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
/** Sorts version strings newest first, leaving anything unparsable at the end. */
|
|
925
|
-
function sortVersions(versions: string[]): string[] {
|
|
926
|
-
return [...versions].sort((left, right) => semver.rcompare(left, right, { loose: true }));
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
/**
|
|
930
|
-
* Builds the error for a ref that no added repository publishes. It names the
|
|
931
|
-
* repositories that were searched, because "not found" almost always means
|
|
932
|
-
* "the repository publishing it has not been added yet".
|
|
933
|
-
*
|
|
934
|
-
* @param item - The request that could not be resolved.
|
|
935
|
-
* @param context - The cached indexes and added repositories.
|
|
936
|
-
* @param what - Whether a namespace or a recipe was being looked for.
|
|
937
|
-
*/
|
|
938
|
-
function unknownRefError(
|
|
939
|
-
item: WorkItem,
|
|
940
|
-
context: ResolveContext,
|
|
941
|
-
what: "namespace" | "recipe"
|
|
942
|
-
): ConfigError {
|
|
943
|
-
const written = item.remote?.written ?? formatRef(item.ref);
|
|
944
|
-
const searched = [...context.indexes.keys()];
|
|
945
|
-
const where =
|
|
946
|
-
searched.length === 0
|
|
947
|
-
? " This project has added no repositories yet."
|
|
948
|
-
: ` Repositories searched: ${searched.join(", ")}.`;
|
|
949
|
-
const asker =
|
|
950
|
-
item.requestedBy === PROJECT_REQUESTER
|
|
951
|
-
? ""
|
|
952
|
-
: `\n It was required by the recipe '${item.requestedBy}'.`;
|
|
953
|
-
|
|
954
|
-
// When the ref said WHICH repository, the useful answer is what that
|
|
955
|
-
// repository does publish: a dependency that names something it has never
|
|
956
|
-
// heard of is almost always a typo or a recipe that was renamed.
|
|
957
|
-
const named = repoOf(item.ref)?.name;
|
|
958
|
-
const index = named === undefined ? undefined : context.indexes.get(named);
|
|
959
|
-
const publishes =
|
|
960
|
-
index === undefined
|
|
961
|
-
? ""
|
|
962
|
-
: `\n The repository '${named}' publishes: ${
|
|
963
|
-
Object.keys(index.recipes).length === 0
|
|
964
|
-
? "nothing yet"
|
|
965
|
-
: Object.keys(index.recipes).sort().join(", ")
|
|
966
|
-
}.`;
|
|
967
|
-
|
|
968
|
-
return new ConfigError(
|
|
969
|
-
`No added repository publishes the ${what} '${written}'.\n` +
|
|
970
|
-
`${where}${asker}${publishes}\n` +
|
|
971
|
-
` Add the repository that publishes it with 'sous repo add <url>', then try again.`
|
|
972
|
-
);
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
/**
|
|
976
|
-
* Builds the error for a ref that resolves in more than one repository. Sous
|
|
977
|
-
* never picks a winner; it shows the qualified form for each repository and
|
|
978
|
-
* asks which one was meant.
|
|
979
|
-
*
|
|
980
|
-
* @param item - The ambiguous request.
|
|
981
|
-
* @param repos - The repositories that publish it.
|
|
982
|
-
*/
|
|
983
|
-
function ambiguousRefError(item: WorkItem, repos: string[]): ConfigError {
|
|
984
|
-
const key = shortKey(item.ref);
|
|
985
|
-
const itemRange = rangeOf(item.ref);
|
|
986
|
-
const range = itemRange === undefined ? "" : `@${itemRange}`;
|
|
987
|
-
const qualified = repos.map((repo) => ` ${repo}:${key}${range}`).join("\n");
|
|
988
|
-
const asker =
|
|
989
|
-
item.requestedBy === PROJECT_REQUESTER
|
|
990
|
-
? ""
|
|
991
|
-
: ` It was required by the recipe '${item.requestedBy}'.`;
|
|
992
|
-
|
|
993
|
-
return new ConfigError(
|
|
994
|
-
`The ref '${key}' is published by more than one added repository: ${repos.join(", ")}.` +
|
|
995
|
-
`${asker}\n` +
|
|
996
|
-
` Say which repository you mean by qualifying the ref with its name:\n${qualified}`
|
|
997
|
-
);
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
/**
|
|
1001
|
-
* Finds the chain of recipes leading from a recipe back to itself, or undefined
|
|
1002
|
-
* when there is none. A cycle is reported rather than treated as an error: the
|
|
1003
|
-
* closure is still finite, and two recipes that co-subscribe to each other are
|
|
1004
|
-
* unusual but not broken.
|
|
1005
|
-
*
|
|
1006
|
-
* @param key - The recipe reached a second time.
|
|
1007
|
-
* @param from - The recipe that reached it.
|
|
1008
|
-
* @param parents - Who pulled each recipe in.
|
|
1009
|
-
*/
|
|
1010
|
-
function findCycle(
|
|
1011
|
-
key: string,
|
|
1012
|
-
from: string,
|
|
1013
|
-
parents: Map<string, string>
|
|
1014
|
-
): string[] | undefined {
|
|
1015
|
-
if (from === PROJECT_REQUESTER) return undefined;
|
|
1016
|
-
|
|
1017
|
-
const chain = [key];
|
|
1018
|
-
let current: string | undefined = from;
|
|
1019
|
-
const guard = new Set<string>();
|
|
1020
|
-
while (current !== undefined && current !== PROJECT_REQUESTER) {
|
|
1021
|
-
chain.push(current);
|
|
1022
|
-
if (current === key) return chain.reverse();
|
|
1023
|
-
if (guard.has(current)) return undefined;
|
|
1024
|
-
guard.add(current);
|
|
1025
|
-
current = parents.get(current);
|
|
1026
|
-
}
|
|
1027
|
-
return undefined;
|
|
1028
|
-
}
|