@cratis/pi 7.103.4 → 7.103.6
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/package/corpus/harnesses/copilot/instructions/local-work-artifacts.instructions.md +7 -7
- package/package/corpus/harnesses/copilot/instructions/specs.csharp.instructions.md +7 -5
- package/package/corpus/hooks/README.md +16 -0
- package/package/corpus/hooks/scripts/cratis-cleanup-worktrees.sh +148 -24
- package/package/corpus/rules/local-work-artifacts.md +7 -7
- package/package/corpus/rules/specs.csharp.md +7 -5
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/SKILL.md +12 -4
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/dependency-map.md +76 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/dotnet-nuget.md +3 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/npm-yarn.md +6 -4
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/train-mode.md +43 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/worked-examples.md +166 -28
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/scripts/cratis-local-packages.sh +1065 -60
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/scripts/dependency-map.tsv +68 -0
- package/package/corpus/skills/cratis-specifications-csharp/SKILL.md +20 -13
- package/package/corpus/skills/cratis-specifications-csharp/references/running-specs.md +90 -0
- package/package/corpus/skills/cratis-specifications-csharp/references/shared-lifecycle.md +55 -23
- package/package/corpus/skills/cratis-specifications-csharp/verification.json +5 -3
- package/package/corpus/skills/filter-syntax/SKILL.md +3 -1
- package/package/corpus/skills/run-tests/SKILL.md +1 -1
- package/package.json +1 -1
|
@@ -10,22 +10,68 @@ Portable local package verification for macOS/Linux (Bash 3.2+). Windows: use WS
|
|
|
10
10
|
Exit codes: 0 success, 1 findings/failed operation, 2 cannot run/invalid input.
|
|
11
11
|
|
|
12
12
|
Commands (COMMAND --help shows these instructions too):
|
|
13
|
-
stamp PRODUCER
|
|
13
|
+
stamp PRODUCER [PRODUCER...]
|
|
14
14
|
Next patch of the highest reachable vX.Y.Z tag, UTC minute and HEAD sha7.
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
With several producers the highest next patch wins (one stamp for a multi-hop
|
|
16
|
+
chain, so no stamped package sorts below a released pin) and sha7 is the last
|
|
17
|
+
producer's; every HEAD is printed to stderr.
|
|
18
|
+
stamp --train PRODUCER [PRODUCER...]
|
|
19
|
+
Train stamp X.Y.Z-train.g<sha7>: the same next patch, no minute, so one code
|
|
20
|
+
state always yields one stamp. Accepted everywhere a local stamp is.
|
|
21
|
+
closure [--map FILE] [--train] CONSUMER=PATH PRODUCER=PATH...
|
|
22
|
+
Read-only. Print, from the same plan, the commands to run in order: one stamp
|
|
23
|
+
(--train: the train stamp), per producer the NuGet overlay of its upstream
|
|
24
|
+
producers, one pack command per map pack row (with its --property arguments)
|
|
25
|
+
and the restore, then ONE verify-pack for the whole plan, then the consumer
|
|
26
|
+
overlay, its verify-assets, restore, check and clean. List producers in dependency order.
|
|
27
|
+
Nothing is printed when the plan is invalid (exit 2). Warns when a producer
|
|
28
|
+
reaches the consumer through an unnamed hop.
|
|
29
|
+
plan [--map FILE] CONSUMER PRODUCER...
|
|
30
|
+
Read-only. Print the plan derived from scripts/dependency-map.tsv (or
|
|
31
|
+
CRATIS_LOCAL_MAP) for repository names listed in dependency order: pack rows
|
|
32
|
+
(producer, ecosystem), overlay rows (producer, upstream) and consume rows
|
|
33
|
+
(the consumer's own edges). Exit 2, nothing printed: unknown or repeated name,
|
|
34
|
+
no edge, wrong order, a planned pack without ids or pack rows.
|
|
35
|
+
verify-pack STAMP --consumer CONSUMER --producer PRODUCER... [--json] [--map FILE]
|
|
36
|
+
Check exactly the plan that "plan" prints for those names: every planned package
|
|
37
|
+
exists in the feed at exactly STAMP (never another stamp), and each nuspec
|
|
38
|
+
dependency on another planned NuGet ID is exactly STAMP, [STAMP] or [STAMP, )
|
|
39
|
+
(an unreadable element fails). Run once,
|
|
40
|
+
after the last pack. Exit 1 lists the findings ("missing: ..." / "dependency: ...").
|
|
41
|
+
Exit 2: invalid plan, or a plan without pack rows (image-only). --json prints one
|
|
42
|
+
line, {"stamp":..,"packages":[{"id":..,"version":..}]}, and sends findings to stderr.
|
|
43
|
+
verify-pack STAMP NAME... [--ecosystem nuget|npm] [--json]
|
|
44
|
+
Manual, weaker form: the IDs the map lists for each named repository (optionally
|
|
45
|
+
one ecosystem) exist at STAMP and their planned NuGet dependencies point at STAMP.
|
|
46
|
+
verify-assets CONSUMER-PATH STAMP [--npm] [--dir REL] [--json] --consumer C --producer P...
|
|
47
|
+
Checks the same plan inside the consumer. Exit 1: a planned package missing from
|
|
48
|
+
the feed; a planned ID resolved at another version ("ID VERSION PATH"); a producer
|
|
49
|
+
the consumer uses in this ecosystem with none of its IDs resolved ("unconsumed:").
|
|
50
|
+
NuGet reads obj/project.assets.json (none: exit 2); --npm reads installed copies
|
|
51
|
+
under node_modules (under REL if given). --json: the consumer's resolved ids and versions.
|
|
52
|
+
verify-assets CONSUMER-PATH STAMP [--npm] [--dir REL] [--json]
|
|
53
|
+
Manual, weaker form without a plan: every package at STAMP in the feed that the
|
|
54
|
+
consumer resolves must be STAMP (none resolved: exit 1).
|
|
55
|
+
pack-nuget PRODUCER STAMP [--property NAME=VALUE]... PROJECT-OR-SLN...
|
|
56
|
+
Release-pack each target into the NuGet feed; requires dotnet. Each --property
|
|
57
|
+
becomes one -p:NAME=VALUE argument of that command only (closure prints the
|
|
58
|
+
properties of each map pack row); no environment variable carries them.
|
|
17
59
|
pack-npm PRODUCER STAMP
|
|
18
|
-
Requires node and Yarn 4, and a completed repository build. Temporarily
|
|
19
|
-
version non-private workspaces, pack with
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
60
|
+
Requires node, npm and Yarn 4, and a completed repository build. Temporarily
|
|
61
|
+
version non-private workspaces, pack each with npm pack --ignore-scripts (the
|
|
62
|
+
publish workflows' tool, so the tarball matches the published one) as
|
|
63
|
+
<scope>-<name>-<version>.tgz, and restore tracked manifests even on failure.
|
|
64
|
+
This helper does not build or run lifecycle scripts.
|
|
65
|
+
use-nuget CONSUMER STAMP [--pin-transitive] [PACKAGE-ID...]
|
|
23
66
|
Create a CPM overlay importing the repository's own Directory.Packages.props.
|
|
67
|
+
Transitive pinning stays as the consumer sets it; --pin-transitive forces it on so
|
|
68
|
+
stamped IDs the consumer does not pin still take effect (can raise NU1109 floors).
|
|
24
69
|
Defaults to every stamped package ID in the feed. Prints properties and sourceable
|
|
25
70
|
env. With source mapping, also prints the required restore --configfile.
|
|
26
71
|
Non-CPM: manually use an untracked project copy with explicit PackageReference
|
|
27
72
|
versions and dotnet restore --source FEED/nuget; do not edit shipped versions.
|
|
28
|
-
use-npm CONSUMER STAMP
|
|
73
|
+
use-npm CONSUMER STAMP [--dir REL]
|
|
74
|
+
--dir REL names a Yarn project in a subdirectory (restore and clean follow it).
|
|
29
75
|
Requires node and tar. Back up package.json, yarn.lock (including existing edits),
|
|
30
76
|
and git diff; add file-tarball resolutions for every stamped feed package.
|
|
31
77
|
Run yarn install afterward. Only one active npm overlay per session.
|
|
@@ -37,7 +83,7 @@ Commands (COMMAND --help shows these instructions too):
|
|
|
37
83
|
check REPO [--staged]
|
|
38
84
|
Scan added/changed tracked lines (staged-only if requested), plus all contents
|
|
39
85
|
of untracked, non-ignored files. Reports path:line:pattern and file counts.
|
|
40
|
-
clean [CONSUMER] [--stamp STAMP | --all] [--apply]
|
|
86
|
+
clean [CONSUMER] [--stamp STAMP | --all] [--apply] [--dir REL]
|
|
41
87
|
Default: list all local feed/cache/image/session subjects, without deleting.
|
|
42
88
|
--apply deletes listed subjects (docker rmi, never force). Refuses more than
|
|
43
89
|
CRATIS_LOCAL_CLEAN_MAX subjects (default 500), or unresolved npm backups.
|
|
@@ -117,7 +163,7 @@ heavy() {
|
|
|
117
163
|
}
|
|
118
164
|
repo_root() { git -C "$1" rev-parse --show-toplevel 2>/dev/null || die "not a Git repository: $1"; }
|
|
119
165
|
valid_stamp() {
|
|
120
|
-
[[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+-local\.[0-9]{12}\.g[0-9a-f]{7}$ ]] || die "invalid local stamp: $1"
|
|
166
|
+
[[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+-(local\.[0-9]{12}|train)\.g[0-9a-f]{7}$ ]] || die "invalid local stamp: $1"
|
|
121
167
|
}
|
|
122
168
|
session_path() {
|
|
123
169
|
local path git_dir
|
|
@@ -141,7 +187,7 @@ validate_session() {
|
|
|
141
187
|
[ -e "$entry" ] || [ -L "$entry" ] || continue
|
|
142
188
|
[ ! -L "$entry" ] || die "symlink in session: $entry"
|
|
143
189
|
case "${entry##*/}" in
|
|
144
|
-
repo|stamp|env|Directory.Packages.props|nuget.config) [ -f "$entry" ] || die "unexpected session entry: $entry" ;;
|
|
190
|
+
repo|stamp|env|npm-dir|Directory.Packages.props|nuget.config) [ -f "$entry" ] || die "unexpected session entry: $entry" ;;
|
|
145
191
|
npm-backup) [ -d "$entry" ] || die "unexpected session entry: $entry" ;;
|
|
146
192
|
*) die "unrecognized file in session (preserved): $entry" ;;
|
|
147
193
|
esac
|
|
@@ -160,30 +206,70 @@ register_session() {
|
|
|
160
206
|
}
|
|
161
207
|
xml_escape() { printf '%s' "$1" | sed -e 's/\&/\&/g' -e 's/</\</g' -e 's/>/\>/g' -e 's/"/\"/g' -e "s/'/\'/g"; }
|
|
162
208
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
local
|
|
166
|
-
|
|
167
|
-
tag=$(git -C "$repo" tag --merged HEAD | awk '/^v[0-9]+\.[0-9]+\.[0-9]+$/' | sort -t. -k1.2,1n -k2,2n -k3,3n | tail -n 1)
|
|
209
|
+
# Next patch of the highest reachable vX.Y.Z tag; sets NEXT_BASE.
|
|
210
|
+
next_base() {
|
|
211
|
+
local tag major minor patch
|
|
212
|
+
tag=$(git -C "$1" tag --merged HEAD | awk '/^v[0-9]+\.[0-9]+\.[0-9]+$/' | sort -t. -k1.2,1n -k2,2n -k3,3n | tail -n 1)
|
|
168
213
|
if [ -n "$tag" ]; then
|
|
169
214
|
IFS=. read -r major minor patch <<< "${tag#v}"
|
|
170
|
-
|
|
215
|
+
NEXT_BASE="$major.$minor.$((10#$patch + 1))"
|
|
171
216
|
else
|
|
172
|
-
printf 'WARNING: no reachable vX.Y.Z tag; using 0.1.0\n' >&2
|
|
173
|
-
|
|
217
|
+
printf 'WARNING: no reachable vX.Y.Z tag in %s; using 0.1.0\n' "$1" >&2
|
|
218
|
+
NEXT_BASE=0.1.0
|
|
219
|
+
fi
|
|
220
|
+
}
|
|
221
|
+
# True when version $1 sorts above $2 (major.minor.patch).
|
|
222
|
+
base_above() {
|
|
223
|
+
local top
|
|
224
|
+
top=$(printf '%s\n%s\n' "$1" "$2" | sort -t. -k1,1n -k2,2n -k3,3n | tail -n 1)
|
|
225
|
+
[ "$top" = "$1" ] && [ "$1" != "$2" ]
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
stamp() {
|
|
229
|
+
[ $# -ge 1 ] || die 'stamp requires a producer repository'
|
|
230
|
+
local repo base='' sha='' arg train=0 lines='' rname origin
|
|
231
|
+
if [ "$1" = --train ]; then train=1; shift; [ $# -ge 1 ] || die 'stamp --train requires a producer repository'; fi
|
|
232
|
+
for arg in "$@"; do
|
|
233
|
+
repo=$(repo_root "$arg")
|
|
234
|
+
next_base "$repo"
|
|
235
|
+
if [ -z "$base" ] || base_above "$NEXT_BASE" "$base"; then base=$NEXT_BASE; fi
|
|
236
|
+
sha=$(git -C "$repo" rev-parse HEAD) || die 'producer has no HEAD commit'
|
|
237
|
+
if [ $# -gt 1 ]; then printf 'HEAD %s %s\n' "$repo" "$sha" >&2; fi
|
|
238
|
+
# The repository name is its origin's last path part (stable across checkouts), else its folder name.
|
|
239
|
+
origin=$(git -C "$repo" remote get-url origin 2>/dev/null || true)
|
|
240
|
+
rname=$(basename "${origin:-$repo}"); rname=${rname%.git}
|
|
241
|
+
lines="$lines$rname $sha"$'\n'
|
|
242
|
+
done
|
|
243
|
+
# With several producers the highest base wins, so no stamped package sorts
|
|
244
|
+
# below a released pin of another stamped package (NU1605).
|
|
245
|
+
# A train stamp has no minute and its sha7 covers every producer: the first 7 hex characters of the
|
|
246
|
+
# SHA-1 of the sorted "<repo> <full HEAD sha>" lines. One combined code state always yields the same
|
|
247
|
+
# stamp, and a change in any producer changes it. The grammar stays X.Y.Z-train.g<sha7>.
|
|
248
|
+
if [ "$train" -eq 1 ]; then
|
|
249
|
+
sha=$(printf '%s' "$lines" | LC_ALL=C sort -u | shasum -a 1) || die 'cannot hash the producer HEADs'
|
|
250
|
+
printf '%s-train.g%.7s\n' "$base" "${sha%% *}"; return 0
|
|
174
251
|
fi
|
|
175
|
-
|
|
176
|
-
printf '%s-local.%s.g%.7s\n' "$tag" "$(date -u +%Y%m%d%H%M)" "$sha"
|
|
252
|
+
printf '%s-local.%s.g%.7s\n' "$base" "$(date -u +%Y%m%d%H%M)" "$sha"
|
|
177
253
|
}
|
|
178
254
|
|
|
179
255
|
pack_nuget() {
|
|
180
256
|
[ $# -ge 3 ] || die 'pack-nuget requires producer, stamp and targets'
|
|
181
|
-
local repo version target path count=0
|
|
257
|
+
local repo version target path count=0 property
|
|
258
|
+
local properties=()
|
|
182
259
|
repo=$(repo_root "$1"); version=$2; shift 2
|
|
183
260
|
valid_stamp "$version"; need dotnet
|
|
261
|
+
# Extra MSBuild properties (from the dependency map) go on the command line, never the environment:
|
|
262
|
+
# an environment variable also reaches nested builds such as Chronicle's `dotnet run` code generator.
|
|
263
|
+
while [ "${1:-}" = --property ]; do
|
|
264
|
+
[ $# -ge 2 ] || die '--property requires NAME=VALUE'
|
|
265
|
+
property=$2; shift 2
|
|
266
|
+
[[ "$property" =~ ^[A-Za-z_][A-Za-z0-9_]*=[A-Za-z0-9_.,-]*$ ]] || die "invalid pack property: $property"
|
|
267
|
+
properties+=("-p:$property")
|
|
268
|
+
done
|
|
269
|
+
[ $# -ge 1 ] || die 'pack-nuget requires at least one target after the properties'
|
|
184
270
|
mutate mkdir -p "$FEED/nuget"
|
|
185
271
|
for target in "$@"; do
|
|
186
|
-
(cd "$repo" && heavy build "pack NuGet $target" dotnet pack "$target" -c Release "-p:PackageVersion=$version" "-p:Version=$version" -o "$FEED/nuget")
|
|
272
|
+
(cd "$repo" && heavy build "pack NuGet $target" dotnet pack "$target" -c Release "-p:PackageVersion=$version" "-p:Version=$version" ${properties[@]+"${properties[@]}"} -o "$FEED/nuget")
|
|
187
273
|
done
|
|
188
274
|
[ "$DRY" -eq 0 ] || return 0
|
|
189
275
|
while IFS= read -r -d '' path; do
|
|
@@ -193,11 +279,27 @@ pack_nuget() {
|
|
|
193
279
|
printf 'produced %s nupkgs\n' "$count"
|
|
194
280
|
}
|
|
195
281
|
|
|
282
|
+
# Pack one workspace with npm pack (what publish does) into the feed as <scope>-<name>-<version>.tgz.
|
|
283
|
+
pack_npm_workspace() {
|
|
284
|
+
local repo=$1 manifest=$2 version=$3 dir name slug
|
|
285
|
+
dir=$repo/$(dirname "$manifest")
|
|
286
|
+
name=$(node -p "require(process.argv[1]).name" "$dir/package.json") || failed "cannot read package name: $manifest"
|
|
287
|
+
slug=${name//\//-}
|
|
288
|
+
if [ "$DRY" -eq 1 ]; then
|
|
289
|
+
quote_command npm pack --ignore-scripts --pack-destination "$FEED/npm" "(in $dir)"
|
|
290
|
+
return 0
|
|
291
|
+
fi
|
|
292
|
+
# Packing is a file copy, not a build: no queue slot per workspace.
|
|
293
|
+
(cd "$dir" && mutate npm pack --ignore-scripts --pack-destination "$FEED/npm") >/dev/null
|
|
294
|
+
[ "$FEED/npm/${slug#@}-$version.tgz" = "$FEED/npm/$slug-$version.tgz" ] || \
|
|
295
|
+
mv -f "$FEED/npm/${slug#@}-$version.tgz" "$FEED/npm/$slug-$version.tgz" || failed "npm pack did not produce $name"
|
|
296
|
+
}
|
|
297
|
+
|
|
196
298
|
pack_npm() {
|
|
197
299
|
[ $# -eq 2 ] || die 'pack-npm requires producer and stamp'
|
|
198
300
|
local repo version path count=0
|
|
199
301
|
repo=$(repo_root "$1"); version=$2
|
|
200
|
-
valid_stamp "$version"; need node; need yarn; scratch
|
|
302
|
+
valid_stamp "$version"; need node; need npm; need yarn; scratch
|
|
201
303
|
git -C "$repo" diff --name-only HEAD -- 'package.json' '*/package.json' > "$TMP/modified"
|
|
202
304
|
git -C "$repo" diff --cached --name-only -- 'package.json' '*/package.json' >> "$TMP/modified"
|
|
203
305
|
git -C "$repo" diff --name-only -- 'package.json' '*/package.json' >> "$TMP/modified"
|
|
@@ -234,7 +336,10 @@ for (const file of files) {
|
|
|
234
336
|
NODE
|
|
235
337
|
fi
|
|
236
338
|
mutate mkdir -p "$FEED/npm"
|
|
237
|
-
|
|
339
|
+
# npm pack, like the publish workflows' npm publish: yarn pack drops files of a gitignored dist folder.
|
|
340
|
+
while IFS= read -r -d '' path; do
|
|
341
|
+
pack_npm_workspace "$repo" "$path" "$version"
|
|
342
|
+
done < "$TMP/manifests"
|
|
238
343
|
if [ "$DRY" -eq 1 ]; then quote_command git -C "$repo" checkout -- '<versioned manifests>'; return 0; fi
|
|
239
344
|
git -C "$repo" checkout -- "${PACK_MANIFESTS[@]}"
|
|
240
345
|
PACK_MANIFESTS=()
|
|
@@ -294,9 +399,13 @@ source_mapping() {
|
|
|
294
399
|
|
|
295
400
|
use_nuget() {
|
|
296
401
|
[ $# -ge 2 ] || die 'use-nuget requires consumer and stamp'
|
|
297
|
-
local repo version session props id file config='' props_key sources_key action
|
|
402
|
+
local repo version session props id file config='' props_key sources_key action pin=0 args=()
|
|
298
403
|
local ids=()
|
|
299
404
|
repo=$(repo_root "$1"); version=$2; shift 2
|
|
405
|
+
for id in "$@"; do
|
|
406
|
+
if [ "$id" = --pin-transitive ]; then pin=1; else args+=("$id"); fi
|
|
407
|
+
done
|
|
408
|
+
set -- ${args[@]+"${args[@]}"}
|
|
300
409
|
valid_stamp "$version"; props=$repo/Directory.Packages.props
|
|
301
410
|
[ -f "$props" ] || die 'no Directory.Packages.props (non-CPM). Manual alternative: untracked project copy, explicit PackageReference versions, dotnet restore --source FEED/nuget'
|
|
302
411
|
session=$(session_path "$repo"); scratch
|
|
@@ -335,7 +444,10 @@ use_nuget() {
|
|
|
335
444
|
register_session "$repo" "$session" "$version"
|
|
336
445
|
{
|
|
337
446
|
printf '<Project>\n <Import Project="%s" />\n' "$(xml_escape "$props")"
|
|
338
|
-
|
|
447
|
+
if [ "$pin" -eq 1 ]; then
|
|
448
|
+
printf ' <PropertyGroup><CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled></PropertyGroup>\n'
|
|
449
|
+
fi
|
|
450
|
+
printf ' <ItemGroup>\n'
|
|
339
451
|
for id in "${ids[@]}"; do
|
|
340
452
|
action=Include
|
|
341
453
|
if grep -Fxiq -- "$id" "$TMP/consumer-ids"; then action=Update; fi
|
|
@@ -359,15 +471,488 @@ use_nuget() {
|
|
|
359
471
|
printf 'overrode %s NuGet package IDs\n' "${#ids[@]}"
|
|
360
472
|
}
|
|
361
473
|
|
|
474
|
+
# Dependency map: tab-separated rows, '#' comments (see references/dependency-map.md).
|
|
475
|
+
# Everything below is derived from these rows; no function reads a feed listing to decide what to expect.
|
|
476
|
+
MAP=${CRATIS_LOCAL_MAP:-$(dirname "$SELF")/dependency-map.tsv}
|
|
477
|
+
map_require() {
|
|
478
|
+
[ -f "$MAP" ] || die "dependency map not found: $MAP"
|
|
479
|
+
awk -F'\t' '
|
|
480
|
+
/^#/ || NF == 0 { next }
|
|
481
|
+
$2 == "" { bad = 1 }
|
|
482
|
+
$1 == "pack" && (NF == 4 || NF == 5) && ($3 == "nuget" || $3 == "npm") && $4 != "" && $2 != "" { next }
|
|
483
|
+
$1 == "ids" && NF == 4 && ($3 == "nuget" || $3 == "npm") && $4 != "" && $2 != "" { next }
|
|
484
|
+
$1 == "uses" && NF == 5 && ($4 == "nuget" || $4 == "npm" || $4 == "image") && $3 != "" && $5 != "" && $2 != "" { next }
|
|
485
|
+
$1 == "image" && NF == 4 && $2 != "" { next }
|
|
486
|
+
{ printf "%s:%d: malformed dependency map row: %s\n", FILENAME, NR, $0 > "/dev/stderr"; bad = 1 }
|
|
487
|
+
END { exit bad ? 1 : 0 }' "$MAP" || exit 2
|
|
488
|
+
}
|
|
489
|
+
map_rows() { awk -F'\t' -v kind="$1" '/^#/ || NF == 0 { next } $1 == kind' "$MAP"; }
|
|
490
|
+
map_known() { awk -F'\t' -v n="$1" '/^#/ || NF == 0 { next } $2 == n || ($1 == "uses" && $3 == n) { found = 1 } END { exit !found }' "$MAP"; }
|
|
491
|
+
# Edge rows "eco<TAB>manifest" for consumer $1 using producer $2.
|
|
492
|
+
map_edge() { map_rows uses | awk -F'\t' -v c="$1" -v p="$2" '$2 == c && $3 == p { print $4 "\t" $5 }'; }
|
|
493
|
+
# Expected package IDs of producer $1 in ecosystem $2, one per line; return 1 when the map has no ids row.
|
|
494
|
+
map_ids() {
|
|
495
|
+
local rows
|
|
496
|
+
rows=$(map_rows ids | awk -F'\t' -v r="$1" -v e="$2" '$2 == r && $3 == e { n = split($4, a, ","); for (i = 1; i <= n; i++) if (a[i] != "") print a[i] }')
|
|
497
|
+
[ -n "$rows" ] || return 1
|
|
498
|
+
printf '%s\n' "$rows"
|
|
499
|
+
}
|
|
500
|
+
# Pack rows of producer $1 in ecosystem $2 as "target<TAB>PROP=VAL,..." (properties may be empty).
|
|
501
|
+
map_pack_rows() { map_rows pack | awk -F'\t' -v r="$1" -v e="$2" '$2 == r && $3 == e { print $4 "\t" $5 }'; }
|
|
502
|
+
# True when the manifest rows of uses edge "$1 uses $2" include ecosystem $3.
|
|
503
|
+
map_uses() { map_edge "$1" "$2" | awk -F'\t' -v e="$3" '$1 == e { found = 1 } END { exit !found }'; }
|
|
504
|
+
|
|
505
|
+
# plan_derive OUTFILE CONSUMER PRODUCER...: the one plan every other command works from. Producers are in
|
|
506
|
+
# dependency order. Rows (TSV) are written to OUTFILE only when the whole plan is valid; any problem is die (exit 2).
|
|
507
|
+
# Call it directly, never inside $(...) or a pipeline, so the exit cannot be lost in a subshell.
|
|
508
|
+
# pack <producer> <eco> eco nuget|npm: a named downstream repository uses it in that ecosystem
|
|
509
|
+
# overlay <producer> <upstream> producer consumes an earlier-named upstream through NuGet
|
|
510
|
+
# consume <producer> <eco> <path> the consumer's own edge (eco nuget|npm|image) and its manifest
|
|
511
|
+
plan_derive() {
|
|
512
|
+
local out=$1 consumer=$2 i j eco name edge manifests
|
|
513
|
+
shift 2
|
|
514
|
+
[ $# -ge 1 ] || die 'a plan needs a consumer and at least one producer'
|
|
515
|
+
local names=("$consumer" "$@")
|
|
516
|
+
map_require
|
|
517
|
+
for ((i=0; i<${#names[@]}; i++)); do
|
|
518
|
+
map_known "${names[$i]}" || die "repository not in the dependency map: ${names[$i]}"
|
|
519
|
+
for ((j=i+1; j<${#names[@]}; j++)); do
|
|
520
|
+
[ "${names[$i]}" != "${names[$j]}" ] || die "repository named twice (a consumer cannot also be a producer): ${names[$i]}"
|
|
521
|
+
done
|
|
522
|
+
done
|
|
523
|
+
for ((i=1; i<${#names[@]}; i++)); do
|
|
524
|
+
for ((j=i+1; j<${#names[@]}; j++)); do
|
|
525
|
+
[ -z "$(map_edge "${names[$i]}" "${names[$j]}")" ] || die "order: ${names[$i]} uses ${names[$j]}; list ${names[$j]} first"
|
|
526
|
+
done
|
|
527
|
+
done
|
|
528
|
+
: > "$out"
|
|
529
|
+
for ((i=1; i<${#names[@]}; i++)); do
|
|
530
|
+
name=${names[$i]}
|
|
531
|
+
edge=0
|
|
532
|
+
for eco in nuget npm image; do
|
|
533
|
+
for ((j=0; j<${#names[@]}; j++)); do
|
|
534
|
+
[ "$j" -eq 0 ] || [ "$j" -gt "$i" ] || continue
|
|
535
|
+
if map_uses "${names[$j]}" "$name" "$eco"; then
|
|
536
|
+
edge=1
|
|
537
|
+
if [ "$eco" != image ]; then plan_add_pack "$out" "$name" "$eco"; fi
|
|
538
|
+
break
|
|
539
|
+
fi
|
|
540
|
+
done
|
|
541
|
+
done
|
|
542
|
+
[ "$edge" -eq 1 ] || die "no edge: nothing named after $name uses it (check the order and the map)"
|
|
543
|
+
done
|
|
544
|
+
for ((i=2; i<${#names[@]}; i++)); do
|
|
545
|
+
for ((j=1; j<i; j++)); do
|
|
546
|
+
if map_uses "${names[$i]}" "${names[$j]}" nuget; then printf 'overlay\t%s\t%s\n' "${names[$i]}" "${names[$j]}" >> "$out"; fi
|
|
547
|
+
done
|
|
548
|
+
done
|
|
549
|
+
for ((i=1; i<${#names[@]}; i++)); do
|
|
550
|
+
map_edge "$consumer" "${names[$i]}" | awk -F'\t' -v p="${names[$i]}" '{ print "consume\t" p "\t" $1 "\t" $2 }' >> "$out"
|
|
551
|
+
done
|
|
552
|
+
manifests=$(awk -F'\t' '$1 == "consume" && $3 == "npm" { print $4 }' "$out" | sort -u | wc -l | tr -d ' ')
|
|
553
|
+
[ "$manifests" -le 1 ] || die "$consumer uses npm producers from different directories; only one npm overlay per session is supported"
|
|
554
|
+
plan_warn_unnamed_hops "${names[@]}"
|
|
555
|
+
}
|
|
556
|
+
# Append "pack <producer> <eco>" after checking the map can verify it (an ids row) and build it (a pack row).
|
|
557
|
+
plan_add_pack() {
|
|
558
|
+
map_ids "$2" "$3" > /dev/null || die "no ids row for $2 ($3): cannot verify the planned pack"
|
|
559
|
+
[ -n "$(map_pack_rows "$2" "$3")" ] || die "no pack row for $2 ($3) in the map"
|
|
560
|
+
printf 'pack\t%s\t%s\n' "$2" "$3" >> "$1"
|
|
561
|
+
}
|
|
562
|
+
# A producer that reaches the consumer through a repository that is not named keeps the released copy of it.
|
|
563
|
+
plan_warn_unnamed_hops() {
|
|
564
|
+
local consumer=$1 kind x p eco manifest named
|
|
565
|
+
shift
|
|
566
|
+
named=$(printf '%s\n' "$@")
|
|
567
|
+
map_rows uses | while IFS=$'\t' read -r kind x p eco manifest; do
|
|
568
|
+
[ "$kind" = uses ] && [ "$eco" != '' ] || continue
|
|
569
|
+
printf '%s\n' "$named" | grep -Fxq "$x" && continue
|
|
570
|
+
printf '%s\n' "$named" | grep -Fxq "$p" || continue
|
|
571
|
+
[ "$p" != "$consumer" ] || continue
|
|
572
|
+
[ -n "$(map_edge "$consumer" "$x")" ] || continue
|
|
573
|
+
printf 'WARNING: %s -> %s -> %s: %s is not named, so the released %s is kept; name it to rebuild it (a binary break is a finding)\n' "$p" "$x" "$consumer" "$x" "$x" >&2
|
|
574
|
+
done
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
# closure [--map FILE] [--train] CONSUMER=PATH PRODUCER=PATH...
|
|
578
|
+
# A printer over plan_derive: it decides nothing the plan does not already say, and prints nothing until the
|
|
579
|
+
# whole plan is valid. Every verify command it prints names the consumer and ALL producers in the given order,
|
|
580
|
+
# so each one re-derives the identical plan (no state is stored between commands).
|
|
581
|
+
# shellcheck disable=SC2016 # printed commands keep a literal $STAMP
|
|
582
|
+
closure() {
|
|
583
|
+
local arg name path i j eco row target props prop manifest mapopt='' train='' overlaid scope='' upstreams dirarg=''
|
|
584
|
+
local names=() paths=()
|
|
585
|
+
while [ $# -gt 0 ]; do
|
|
586
|
+
arg=$1; shift
|
|
587
|
+
case "$arg" in
|
|
588
|
+
--map) [ $# -gt 0 ] || die '--map requires a file'; MAP=$1; printf -v mapopt ' --map %q' "$1"; shift ;;
|
|
589
|
+
--train) train=' --train' ;;
|
|
590
|
+
*=*) name=${arg%%=*}; path=${arg#*=}; names+=("$name"); paths+=("$(repo_root "$path")") ;;
|
|
591
|
+
*) die "closure arguments are NAME=PATH; got: $arg" ;;
|
|
592
|
+
esac
|
|
593
|
+
done
|
|
594
|
+
[ ${#names[@]} -ge 2 ] || die 'closure requires CONSUMER=PATH and at least one PRODUCER=PATH'
|
|
595
|
+
scratch
|
|
596
|
+
plan_derive "$TMP/closure-plan" "${names[@]}"
|
|
597
|
+
for ((i=1; i<${#names[@]}; i++)); do printf -v arg ' --producer %q' "${names[$i]}"; scope="$scope$arg"; done
|
|
598
|
+
scope=" --consumer $(printf '%q' "${names[0]}")$scope$mapopt"
|
|
599
|
+
printf '# closure for %s (printed, not executed; run each step and check its exit code)\n' "${names[0]}"
|
|
600
|
+
printf 'STAMP=$(bash %q stamp%s' "$SELF" "$train"
|
|
601
|
+
for ((i=1; i<${#paths[@]}; i++)); do printf ' %q' "${paths[$i]}"; done
|
|
602
|
+
printf ')\n'
|
|
603
|
+
for ((i=1; i<${#names[@]}; i++)); do
|
|
604
|
+
name=${names[$i]}; overlaid=0
|
|
605
|
+
upstreams=$(awk -F'\t' -v p="$name" '$1 == "overlay" && $2 == p { printf "%s ", $3 }' "$TMP/closure-plan")
|
|
606
|
+
if [ -n "$upstreams" ]; then
|
|
607
|
+
overlaid=1
|
|
608
|
+
printf '# %s consumes %s: overlay it, then pack in a shell that sourced the printed env file\n' "$name" "${upstreams% }"
|
|
609
|
+
printf 'bash %q use-nuget %q "$STAMP"\n' "$SELF" "${paths[$i]}"
|
|
610
|
+
fi
|
|
611
|
+
for eco in nuget npm; do
|
|
612
|
+
grep -Fxq "$(printf 'pack\t%s\t%s' "$name" "$eco")" "$TMP/closure-plan" || continue
|
|
613
|
+
if [ "$eco" = npm ]; then
|
|
614
|
+
printf '( cd %q && bash %q pack-npm . "$STAMP" ) # needs yarn install --immutable and the repository build first; pack-npm does not build\n' "${paths[$i]}" "$SELF"
|
|
615
|
+
continue
|
|
616
|
+
fi
|
|
617
|
+
while IFS=$'\t' read -r target props; do
|
|
618
|
+
printf '( cd %q && ' "${paths[$i]}"
|
|
619
|
+
if [ "$overlaid" -eq 1 ]; then printf '. %q && ' "$(session_path "${paths[$i]}")/env"; fi
|
|
620
|
+
printf 'bash %q pack-nuget . "$STAMP"' "$SELF"
|
|
621
|
+
for prop in ${props//,/ }; do printf ' --property %q' "$prop"; done
|
|
622
|
+
printf ' %q )\n' "$target"
|
|
623
|
+
done < <(map_pack_rows "$name" nuget)
|
|
624
|
+
done
|
|
625
|
+
if [ "$overlaid" -eq 1 ]; then printf 'bash %q restore %q\n' "$SELF" "${paths[$i]}"; fi
|
|
626
|
+
for ((j=i+1; j<${#names[@]}; j++)); do
|
|
627
|
+
while IFS=$'\t' read -r eco manifest; do
|
|
628
|
+
case "$eco" in npm|image) printf '# note: %s uses %s (%s, %s): not overlaid here; wire it by hand if the %s build needs the local %s\n' "${names[$j]}" "$name" "$eco" "$manifest" "${names[$j]}" "$name" ;; esac
|
|
629
|
+
done < <(map_edge "${names[$j]}" "$name")
|
|
630
|
+
done
|
|
631
|
+
done
|
|
632
|
+
if grep -q '^pack' "$TMP/closure-plan"; then
|
|
633
|
+
printf 'bash %q verify-pack "$STAMP"%s\n' "$SELF" "$scope"
|
|
634
|
+
else
|
|
635
|
+
printf '# no pack rows in this plan (image-only edges): nothing to verify in the feed\n'
|
|
636
|
+
fi
|
|
637
|
+
for eco in nuget npm image; do
|
|
638
|
+
row=$(awk -F'\t' -v e="$eco" '$1 == "consume" && $3 == e { print $4; exit }' "$TMP/closure-plan")
|
|
639
|
+
[ -n "$row" ] || continue
|
|
640
|
+
if [ "$eco" = npm ] && [ "$row" != . ]; then printf -v dirarg ' --dir %q' "$row"; fi
|
|
641
|
+
case "$eco" in
|
|
642
|
+
nuget) printf 'bash %q use-nuget %q "$STAMP"\n# source the printed env file, restore and build the consumer, then:\nbash %q verify-assets %q "$STAMP"%s\n' "$SELF" "${paths[0]}" "$SELF" "${paths[0]}" "$scope" ;;
|
|
643
|
+
npm) printf 'bash %q use-npm %q "$STAMP"%s # then yarn install (not --immutable)\nbash %q verify-assets %q "$STAMP" --npm%s%s\n' "$SELF" "${paths[0]}" "$dirarg" "$SELF" "${paths[0]}" "$dirarg" "$scope" ;;
|
|
644
|
+
image) printf '# image edge: build the producer image per references/docker-images.md, tag it with "$STAMP"\n' ;;
|
|
645
|
+
esac
|
|
646
|
+
done
|
|
647
|
+
printf 'bash %q restore %q\nbash %q check %q\nbash %q clean %q --stamp "$STAMP"%s # review, then repeat with --apply\n' "$SELF" "${paths[0]}" "$SELF" "${paths[0]}" "$SELF" "${paths[0]}" "$dirarg"
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
# plan [--map FILE] CONSUMER PRODUCER...: print the plan rows (read-only).
|
|
651
|
+
plan_command() {
|
|
652
|
+
local args=()
|
|
653
|
+
while [ $# -gt 0 ]; do
|
|
654
|
+
case "$1" in
|
|
655
|
+
--map) [ $# -ge 2 ] || die '--map requires a file'; MAP=$2; shift 2 ;;
|
|
656
|
+
--*) die "unknown plan option: $1" ;;
|
|
657
|
+
*) args+=("$1"); shift ;;
|
|
658
|
+
esac
|
|
659
|
+
done
|
|
660
|
+
[ ${#args[@]} -ge 2 ] || die 'plan requires CONSUMER and at least one PRODUCER (repository names from the map)'
|
|
661
|
+
scratch
|
|
662
|
+
plan_derive "$TMP/plan" "${args[@]}"
|
|
663
|
+
cat "$TMP/plan"
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
# ---- Verification primitives. Each one reads only what it is passed and writes only to the paths it is given. ----
|
|
667
|
+
# One private scratch directory per command; stateless: nothing survives between two verify invocations.
|
|
668
|
+
SCR=''
|
|
669
|
+
fn_scratch() { scratch; SCR=$(mktemp -d "$TMP/fn.XXXXXX") || die 'cannot create a scratch directory'; }
|
|
670
|
+
|
|
671
|
+
# One line per planned artifact: "id<TAB>eco<TAB>producer". Derived from the plan rows and the map's ids rows only.
|
|
672
|
+
expected_artifacts() {
|
|
673
|
+
local plan=$1 out=$2 kind p eco id
|
|
674
|
+
: > "$out"
|
|
675
|
+
while IFS=$'\t' read -r kind p eco; do
|
|
676
|
+
[ "$kind" = pack ] || continue
|
|
677
|
+
while IFS= read -r id; do printf '%s\t%s\t%s\n' "$id" "$eco" "$p" >> "$out"; done < <(map_ids "$p" "$eco")
|
|
678
|
+
done < "$plan"
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
# The exact feed file of one artifact. Never a glob and never another stamp.
|
|
682
|
+
feed_path() {
|
|
683
|
+
case "$2" in
|
|
684
|
+
nuget) printf '%s/nuget/%s.%s.nupkg\n' "$FEED" "$3" "$1" ;;
|
|
685
|
+
npm) printf '%s/npm/%s-%s.tgz\n' "$FEED" "${3//\//-}" "$1" ;;
|
|
686
|
+
esac
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
# Split EXPECTED into PRESENT_OUT (same rows) and FINDINGS_OUT ("missing: ID STAMP (producer eco)").
|
|
690
|
+
feed_check() {
|
|
691
|
+
local stamp=$1 expected=$2 present=$3 findings=$4 id eco p
|
|
692
|
+
: > "$present"; : > "$findings"
|
|
693
|
+
while IFS=$'\t' read -r id eco p; do
|
|
694
|
+
if [ -f "$(feed_path "$stamp" "$eco" "$id")" ]; then printf '%s\t%s\t%s\n' "$id" "$eco" "$p" >> "$present"
|
|
695
|
+
else printf 'missing: %s %s (%s %s)\n' "$id" "$stamp" "$p" "$eco" >> "$findings"; fi
|
|
696
|
+
done < "$expected"
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
# A dependency matches only when it is exactly the stamp: STAMP, [STAMP] or [STAMP, ) (an inclusive lower bound
|
|
700
|
+
# with no upper bound, which is how a floor is written). Exclusive bounds, upper bounds and anything else fail.
|
|
701
|
+
dep_matches() {
|
|
702
|
+
local range=${2//[[:space:]]/}
|
|
703
|
+
[ "$range" = "$1" ] || [ "$range" = "[$1]" ] || [ "$range" = "[$1,)" ]
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
# "dep<TAB>id<TAB>version" for every <dependency> element of a nuspec, whatever the attribute order, quote
|
|
707
|
+
# style or line breaks; "bad<TAB>element" for one whose id or version cannot be read (the caller fails on it).
|
|
708
|
+
nuspec_dependencies() {
|
|
709
|
+
tr '\r\n\t' ' ' < "$1" | awk '
|
|
710
|
+
function attr(el, n, v) {
|
|
711
|
+
if (match(el, "[ ]" n "[ ]*=[ ]*\"[^\"]*\"")) { v = substr(el, RSTART, RLENGTH); sub(/^[^"]*"/, "", v); sub(/"$/, "", v); return v }
|
|
712
|
+
if (match(el, "[ ]" n "[ ]*=[ ]*\047[^\047]*\047")) { v = substr(el, RSTART, RLENGTH); sub(/^[^\047]*\047/, "", v); sub(/\047$/, "", v); return v }
|
|
713
|
+
return ""
|
|
714
|
+
}
|
|
715
|
+
{ s = $0
|
|
716
|
+
while (match(s, /<dependency([^A-Za-z]|$)/)) {
|
|
717
|
+
rest = substr(s, RSTART); e = index(rest, ">")
|
|
718
|
+
if (e == 0) { print "bad\t" substr(rest, 1, 80); break }
|
|
719
|
+
el = substr(rest, 1, e); s = substr(rest, e + 1)
|
|
720
|
+
id = attr(el, "id"); ver = attr(el, "version")
|
|
721
|
+
if (id == "" || ver == "" || index(substr(el, 2), "<") > 0) print "bad\t" el
|
|
722
|
+
else print "dep\t" id "\t" ver
|
|
723
|
+
} }'
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
# Every nuspec dependency of a present planned .nupkg whose ID is in DEPSET must point at the stamp.
|
|
727
|
+
# Appends to FINDINGS_OUT and sets NUSPEC_EDGES to the number of dependency edges checked.
|
|
728
|
+
NUSPEC_EDGES=0
|
|
729
|
+
nuspec_findings() {
|
|
730
|
+
local stamp=$1 depset=$2 present=$3 findings=$4 id eco p kind dep range nuspec
|
|
731
|
+
NUSPEC_EDGES=0
|
|
732
|
+
nuspec=$findings.nuspec
|
|
733
|
+
while IFS=$'\t' read -r id eco p; do
|
|
734
|
+
[ "$eco" = nuget ] || continue
|
|
735
|
+
if ! unzip -p "$(feed_path "$stamp" nuget "$id")" '*.nuspec' > "$nuspec" 2>/dev/null; then
|
|
736
|
+
printf 'unreadable nuspec: %s\n' "$id" >> "$findings"; continue
|
|
737
|
+
fi
|
|
738
|
+
while IFS=$'\t' read -r kind dep range; do
|
|
739
|
+
if [ "$kind" = bad ]; then printf 'unparseable dependency: %s: %s\n' "$id" "$dep" >> "$findings"; continue; fi
|
|
740
|
+
grep -Fxiq -- "$dep" "$depset" || continue
|
|
741
|
+
NUSPEC_EDGES=$((NUSPEC_EDGES + 1))
|
|
742
|
+
dep_matches "$stamp" "$range" || printf 'dependency: %s -> %s %s (expected %s)\n' "$id" "$dep" "$range" "$stamp" >> "$findings"
|
|
743
|
+
done < <(nuspec_dependencies "$nuspec")
|
|
744
|
+
done < "$present"
|
|
745
|
+
rm -f -- "$nuspec"
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
# "id<TAB>version<TAB>assets file" for every package in IDSFILE that the consumer's restore resolved.
|
|
749
|
+
# Library keys are "Id/Version"; project references are not packages. Exit 2 without an assets file.
|
|
750
|
+
nuget_resolutions() {
|
|
751
|
+
local consumer=$1 idsfile=$2 out=$3 path files=()
|
|
752
|
+
while IFS= read -r -d '' path; do files+=("$path"); done < <(find "$consumer" -name node_modules -prune -o -path '*/obj/project.assets.json' -type f -print0)
|
|
753
|
+
[ ${#files[@]} -gt 0 ] || { printf 'ERROR: no obj/project.assets.json under %s (restore the consumer first)\n' "$consumer" >&2; return 2; }
|
|
754
|
+
need node
|
|
755
|
+
node - "$idsfile" "${files[@]}" > "$out" <<'NODE' || { printf 'ERROR: cannot read the consumer assets\n' >&2; return 2; }
|
|
756
|
+
const fs = require('fs');
|
|
757
|
+
const [idsfile, ...files] = process.argv.slice(2);
|
|
758
|
+
const ids = new Set(fs.readFileSync(idsfile, 'utf8').split('\n').filter(Boolean).map(s => s.toLowerCase()));
|
|
759
|
+
for (const file of files) {
|
|
760
|
+
let json;
|
|
761
|
+
try { json = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (e) { console.error('invalid JSON: ' + file); process.exit(1); }
|
|
762
|
+
if (!json || typeof json.libraries !== 'object' || json.libraries === null) { console.error('no libraries object: ' + file); process.exit(1); }
|
|
763
|
+
for (const [key, lib] of Object.entries(json.libraries)) {
|
|
764
|
+
const cut = key.indexOf('/');
|
|
765
|
+
if (cut < 1 || cut === key.length - 1 || !lib || typeof lib.type !== 'string') { console.error('unreadable library ' + key + ': ' + file); process.exit(1); }
|
|
766
|
+
if (lib.type !== 'project' && ids.has(key.slice(0, cut).toLowerCase())) console.log(key.slice(0, cut) + '\t' + key.slice(cut + 1) + '\t' + file);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
NODE
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
# "name<TAB>version<TAB>package.json" for every installed copy of a package in IDSFILE under NM.
|
|
773
|
+
npm_installed() {
|
|
774
|
+
local nm=$1 idsfile=$2 out=$3 name file version
|
|
775
|
+
need node
|
|
776
|
+
[ -d "$nm" ] || { printf 'ERROR: no node_modules: %s (run yarn install first)\n' "$nm" >&2; return 2; }
|
|
777
|
+
: > "$out"
|
|
778
|
+
while IFS= read -r name; do
|
|
779
|
+
while IFS= read -r -d '' file; do
|
|
780
|
+
version=$(node -e 'const v = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")).version; if (typeof v !== "string" || v === "") process.exit(1); process.stdout.write(v)' "$file") \
|
|
781
|
+
|| { printf 'ERROR: unreadable package.json (no version): %s\n' "$file" >&2; return 2; }
|
|
782
|
+
printf '%s\t%s\t%s\n' "$name" "$version" "$file" >> "$out"
|
|
783
|
+
done < <(find "$nm" -path "*/node_modules/$name/package.json" -type f -print0)
|
|
784
|
+
done < "$idsfile"
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
# Every producer the consumer uses in ECO must have at least one of its planned IDs resolved.
|
|
788
|
+
# A consumer may use only part of a producer, so one ID is enough.
|
|
789
|
+
consumption_findings() {
|
|
790
|
+
local plan=$1 eco=$2 resolved=$3 findings=$4 kind p e manifest ids
|
|
791
|
+
while IFS=$'\t' read -r kind p e manifest; do
|
|
792
|
+
[ "$kind" = consume ] && [ "$e" = "$eco" ] || continue
|
|
793
|
+
ids=$(map_ids "$p" "$eco") || ids=''
|
|
794
|
+
if ! awk -F'\t' 'NR == FNR { want[tolower($0)] = 1; next } tolower($1) in want { found = 1 } END { exit !found }' <(printf '%s\n' "$ids") "$resolved"; then
|
|
795
|
+
printf 'unconsumed: %s (%s): none of %s resolved in the consumer\n' "$p" "$eco" "$(printf '%s' "$ids" | tr '\n' ',' | sed 's/,$//; s/,/, /g')" >> "$findings"
|
|
796
|
+
fi
|
|
797
|
+
done < "$plan"
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
# One JSON line from tab-separated "id<TAB>version" rows in $2, for machine consumers (pi-train).
|
|
801
|
+
json_report() {
|
|
802
|
+
awk -F'\t' -v stamp="$1" 'BEGIN { printf "{\"stamp\":\"%s\",\"packages\":[", stamp }
|
|
803
|
+
{ printf "%s{\"id\":\"%s\",\"version\":\"%s\"}", (NR > 1 ? "," : ""), $1, $2 }
|
|
804
|
+
END { print "]}" }' "$2"
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
# Print findings to stdout, or stderr when stdout carries JSON, then fail. Returns only when there are none.
|
|
808
|
+
report_findings() {
|
|
809
|
+
local command=$1 findings=$2 out=$3 count
|
|
810
|
+
count=$(wc -l < "$findings" | tr -d ' ')
|
|
811
|
+
[ "$count" -eq 0 ] && return 0
|
|
812
|
+
cat "$findings" >&"$out"
|
|
813
|
+
failed "$command: $count findings"
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
# verify-pack STAMP (--consumer C --producer P... | NAME... [--ecosystem E]) [--json] [--map FILE]
|
|
817
|
+
# Plan mode checks exactly the plan the closure printed. Name mode (manual, weaker) builds the plan rows from
|
|
818
|
+
# the named repositories' ids rows instead of from edges.
|
|
819
|
+
verify_pack() {
|
|
820
|
+
[ $# -ge 2 ] || die 'verify-pack requires STAMP and either --consumer C --producer P... or repository NAMEs'
|
|
821
|
+
local stamp=$1 json=0 out=1 ecosystem='' consumer='' name eco d found
|
|
822
|
+
local producers=() names=()
|
|
823
|
+
shift
|
|
824
|
+
while [ $# -gt 0 ]; do
|
|
825
|
+
case "$1" in
|
|
826
|
+
--json) json=1; out=2; shift ;;
|
|
827
|
+
--map) [ $# -ge 2 ] || die '--map requires a file'; MAP=$2; shift 2 ;;
|
|
828
|
+
--consumer) [ $# -ge 2 ] || die '--consumer requires a repository name'; consumer=$2; shift 2 ;;
|
|
829
|
+
--producer) [ $# -ge 2 ] || die '--producer requires a repository name'; producers+=("$2"); shift 2 ;;
|
|
830
|
+
--ecosystem) [ $# -ge 2 ] || die '--ecosystem requires nuget or npm'; ecosystem=$2; shift 2
|
|
831
|
+
case "$ecosystem" in nuget|npm) ;; *) die 'invalid ecosystem' ;; esac ;;
|
|
832
|
+
--*) die "unknown verify-pack option: $1" ;;
|
|
833
|
+
*) names+=("$1"); shift ;;
|
|
834
|
+
esac
|
|
835
|
+
done
|
|
836
|
+
valid_stamp "$stamp"; fn_scratch; d=$SCR
|
|
837
|
+
if [ -n "$consumer" ] || [ ${#producers[@]} -gt 0 ]; then
|
|
838
|
+
[ -n "$consumer" ] && [ ${#producers[@]} -gt 0 ] || die 'verify-pack plan mode needs both --consumer and --producer'
|
|
839
|
+
[ ${#names[@]} -eq 0 ] && [ -z "$ecosystem" ] || die 'verify-pack takes either --consumer/--producer or repository names, not both'
|
|
840
|
+
plan_derive "$d/plan" "$consumer" "${producers[@]}"
|
|
841
|
+
else
|
|
842
|
+
[ ${#names[@]} -ge 1 ] || die 'verify-pack requires --consumer/--producer or at least one repository name from the map'
|
|
843
|
+
map_require
|
|
844
|
+
: > "$d/plan"
|
|
845
|
+
for name in "${names[@]}"; do
|
|
846
|
+
map_known "$name" || die "repository not in the dependency map: $name"
|
|
847
|
+
found=0
|
|
848
|
+
for eco in nuget npm; do
|
|
849
|
+
[ -z "$ecosystem" ] || [ "$eco" = "$ecosystem" ] || continue
|
|
850
|
+
if map_ids "$name" "$eco" > /dev/null; then printf 'pack\t%s\t%s\n' "$name" "$eco" >> "$d/plan"; found=1; fi
|
|
851
|
+
done
|
|
852
|
+
[ "$found" -eq 1 ] || die "no ids row for $name (${ecosystem:-all ecosystems}) in the map"
|
|
853
|
+
done
|
|
854
|
+
fi
|
|
855
|
+
grep -q '^pack' "$d/plan" || die 'the plan has no pack rows (image-only edges): nothing to verify'
|
|
856
|
+
expected_artifacts "$d/plan" "$d/expected"
|
|
857
|
+
awk -F'\t' '$2 == "nuget" { print $1 }' "$d/expected" > "$d/depset"
|
|
858
|
+
[ ! -s "$d/depset" ] || need unzip
|
|
859
|
+
feed_check "$stamp" "$d/expected" "$d/present" "$d/findings"
|
|
860
|
+
nuspec_findings "$stamp" "$d/depset" "$d/present" "$d/findings"
|
|
861
|
+
report_findings verify-pack "$d/findings" "$out"
|
|
862
|
+
if [ "$json" -eq 1 ]; then
|
|
863
|
+
awk -F'\t' -v s="$stamp" '{ print $1 "\t" s }' "$d/present" > "$d/json"
|
|
864
|
+
json_report "$stamp" "$d/json"
|
|
865
|
+
return 0
|
|
866
|
+
fi
|
|
867
|
+
printf 'verify-pack: %s planned artifacts present at %s (%s dependency edges checked)\n' "$(wc -l < "$d/present" | tr -d ' ')" "$stamp" "$NUSPEC_EDGES"
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
# verify-assets CONSUMER-PATH STAMP [--npm] [--dir REL] [--json] [--consumer C --producer P...] [--map FILE]
|
|
871
|
+
# Plan mode scans for exactly the planned IDs of one ecosystem; feed-wide mode (no --consumer) scans every
|
|
872
|
+
# package at the stamp in the feed, which is manual and weaker.
|
|
873
|
+
verify_assets() {
|
|
874
|
+
[ $# -ge 2 ] || die 'verify-assets requires CONSUMER-PATH, STAMP, [--npm] [--dir REL]'
|
|
875
|
+
local path=$1 stamp=$2 npm=0 dir='' json=0 out=1 consumer='' eco=nuget d name file matched nm unit
|
|
876
|
+
local producers=()
|
|
877
|
+
shift 2
|
|
878
|
+
while [ $# -gt 0 ]; do
|
|
879
|
+
case "$1" in
|
|
880
|
+
--npm) npm=1; eco=npm; shift ;;
|
|
881
|
+
--json) json=1; out=2; shift ;;
|
|
882
|
+
--dir) [ $# -ge 2 ] || die '--dir requires a relative directory'; dir=${2%/}; shift 2 ;;
|
|
883
|
+
--map) [ $# -ge 2 ] || die '--map requires a file'; MAP=$2; shift 2 ;;
|
|
884
|
+
--consumer) [ $# -ge 2 ] || die '--consumer requires a repository name'; consumer=$2; shift 2 ;;
|
|
885
|
+
--producer) [ $# -ge 2 ] || die '--producer requires a repository name'; producers+=("$2"); shift 2 ;;
|
|
886
|
+
*) die "unknown verify-assets option: $1" ;;
|
|
887
|
+
esac
|
|
888
|
+
done
|
|
889
|
+
valid_stamp "$stamp"; [ -d "$path" ] || die "not a directory: $path"
|
|
890
|
+
case "$dir" in /*|..|../*|*/..|*/../*) die "--dir must be a relative path inside the consumer: $dir" ;; esac
|
|
891
|
+
if [ -n "$consumer" ] || [ ${#producers[@]} -gt 0 ]; then
|
|
892
|
+
[ -n "$consumer" ] && [ ${#producers[@]} -gt 0 ] || die 'verify-assets needs --consumer and --producer together'
|
|
893
|
+
fi
|
|
894
|
+
fn_scratch; d=$SCR
|
|
895
|
+
: > "$d/findings"
|
|
896
|
+
if [ -n "$consumer" ]; then
|
|
897
|
+
plan_derive "$d/plan" "$consumer" "${producers[@]}"
|
|
898
|
+
awk -F'\t' -v e="$eco" '$1 == "consume" && $3 == e { found = 1 } END { exit !found }' "$d/plan" || die "$consumer has no $eco edge to the named producers"
|
|
899
|
+
expected_artifacts "$d/plan" "$d/all-expected"
|
|
900
|
+
awk -F'\t' -v e="$eco" '$2 == e' "$d/all-expected" > "$d/expected"
|
|
901
|
+
feed_check "$stamp" "$d/expected" "$d/present" "$d/findings"
|
|
902
|
+
cut -f1 "$d/expected" | sort -u > "$d/ids"
|
|
903
|
+
else
|
|
904
|
+
: > "$d/ids"
|
|
905
|
+
if [ "$npm" -eq 1 ]; then
|
|
906
|
+
for file in "$FEED/npm/"*"-$stamp.tgz"; do
|
|
907
|
+
[ -f "$file" ] || continue
|
|
908
|
+
name=$(tar -xOf "$file" package/package.json | node -e 'const n = JSON.parse(require("fs").readFileSync(0, "utf8")).name; if (typeof n !== "string" || n === "") process.exit(1); process.stdout.write(n)') \
|
|
909
|
+
|| die "cannot read the package name: $file"
|
|
910
|
+
printf '%s\n' "$name" >> "$d/ids"
|
|
911
|
+
done
|
|
912
|
+
else
|
|
913
|
+
for file in "$FEED/nuget/"*".$stamp.nupkg"; do
|
|
914
|
+
[ -f "$file" ] || continue
|
|
915
|
+
name=${file##*/}; printf '%s\n' "${name%."$stamp".nupkg}" >> "$d/ids"
|
|
916
|
+
done
|
|
917
|
+
[ -s "$d/ids" ] || die "no packages at $stamp in the feed"
|
|
918
|
+
fi
|
|
919
|
+
fi
|
|
920
|
+
if [ "$npm" -eq 1 ]; then
|
|
921
|
+
nm=$path${dir:+/$dir}/node_modules
|
|
922
|
+
npm_installed "$nm" "$d/ids" "$d/resolved" || return $?
|
|
923
|
+
else
|
|
924
|
+
nuget_resolutions "$path" "$d/ids" "$d/resolved" || return $?
|
|
925
|
+
fi
|
|
926
|
+
matched=$(wc -l < "$d/resolved" | tr -d ' ')
|
|
927
|
+
awk -F'\t' -v s="$stamp" '$2 != s { print $1 " " $2 " " $3 }' "$d/resolved" >> "$d/findings"
|
|
928
|
+
if [ -n "$consumer" ]; then
|
|
929
|
+
consumption_findings "$d/plan" "$eco" "$d/resolved" "$d/findings"
|
|
930
|
+
elif [ "$matched" -eq 0 ]; then
|
|
931
|
+
failed "verify-assets: no stamped $eco package is resolved in the consumer"
|
|
932
|
+
fi
|
|
933
|
+
report_findings verify-assets "$d/findings" "$out"
|
|
934
|
+
if [ "$json" -eq 1 ]; then
|
|
935
|
+
awk -F'\t' '{ print $1 "\t" $2 }' "$d/resolved" | sort -u > "$d/json"
|
|
936
|
+
json_report "$stamp" "$d/json"
|
|
937
|
+
return 0
|
|
938
|
+
fi
|
|
939
|
+
unit=resolutions
|
|
940
|
+
if [ "$npm" -eq 1 ]; then unit='installed copies'; fi
|
|
941
|
+
printf 'verify-assets: %s %s, all at %s\n' "$matched" "$unit" "$stamp"
|
|
942
|
+
}
|
|
943
|
+
|
|
362
944
|
use_npm() {
|
|
363
|
-
[ $# -eq 2 ] || die 'use-npm requires consumer and
|
|
364
|
-
local repo version session backup
|
|
945
|
+
[ $# -eq 2 ] || { [ $# -eq 4 ] && [ "$3" = --dir ]; } || die 'use-npm requires consumer, stamp and optional --dir REL'
|
|
946
|
+
local repo version session backup rel='' dir
|
|
365
947
|
repo=$(repo_root "$1"); version=$2
|
|
948
|
+
if [ $# -eq 4 ]; then rel=${4%/}; fi
|
|
949
|
+
case "$rel" in /*|..|../*|*/..|*/../*) die "--dir must be a relative path inside the consumer: $rel" ;; esac
|
|
950
|
+
dir=$repo${rel:+/$rel}
|
|
366
951
|
valid_stamp "$version"; need node; need tar; scratch
|
|
367
952
|
session=$(session_path "$repo"); backup=$session/npm-backup
|
|
368
|
-
[ -f "$
|
|
953
|
+
[ -f "$dir/package.json" ] || die "consumer has no package.json in ${rel:-the root}; use --dir REL for a Yarn project in a subdirectory"
|
|
369
954
|
[ ! -e "$backup" ] || die "active npm backup exists: $backup; restore first"
|
|
370
|
-
node - "$
|
|
955
|
+
node - "$dir" "$FEED/npm" "$version" "$TMP/package.json" <<'NODE'
|
|
371
956
|
const fs = require('fs'), path = require('path'), {execFileSync} = require('child_process');
|
|
372
957
|
const [repo, feed, stamp, output] = process.argv.slice(2);
|
|
373
958
|
const root = JSON.parse(fs.readFileSync(path.join(repo, 'package.json'), 'utf8'));
|
|
@@ -389,39 +974,42 @@ console.log(`overrode ${count} npm packages`);
|
|
|
389
974
|
NODE
|
|
390
975
|
if [ "$DRY" -eq 1 ]; then
|
|
391
976
|
quote_command mkdir -p "$backup"
|
|
392
|
-
quote_command cp "$TMP/package.json" "$
|
|
977
|
+
quote_command cp "$TMP/package.json" "$dir/package.json"
|
|
393
978
|
return 0
|
|
394
979
|
fi
|
|
395
980
|
register_session "$repo" "$session" "$version"
|
|
396
981
|
mkdir "$backup"
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
982
|
+
if [ -n "$rel" ]; then printf '%s\n' "$rel" > "$session/npm-dir"; fi
|
|
983
|
+
cp -p "$dir/package.json" "$backup/package.json"
|
|
984
|
+
if [ -f "$dir/yarn.lock" ]; then cp -p "$dir/yarn.lock" "$backup/yarn.lock"; fi
|
|
985
|
+
git -C "$dir" diff HEAD -- package.json yarn.lock > "$backup/before.diff"
|
|
400
986
|
cp "$TMP/package.json" "$backup/expected-package.json"
|
|
401
|
-
cp "$TMP/package.json" "$
|
|
402
|
-
printf 'Next: cd %q && yarn install\n' "$
|
|
987
|
+
cp "$TMP/package.json" "$dir/package.json"
|
|
988
|
+
printf 'Next: cd %q && yarn install\n' "$dir"
|
|
403
989
|
printf 'Vite: clear stale prebundles with rm -rf node_modules/.vite\n'
|
|
404
990
|
}
|
|
405
991
|
|
|
406
992
|
restore() {
|
|
407
993
|
[ $# -eq 1 ] || die 'restore requires a consumer'
|
|
408
|
-
local repo session backup
|
|
994
|
+
local repo session backup dir
|
|
409
995
|
repo=$(repo_root "$1"); session=$(session_path "$repo"); backup=$session/npm-backup
|
|
410
996
|
validate_session "$session"
|
|
997
|
+
dir=$repo
|
|
998
|
+
if [ -f "$session/npm-dir" ]; then dir=$repo/$(cat "$session/npm-dir"); fi
|
|
411
999
|
if [ -f "$session/repo" ] && [ "$(cat "$session/repo")" != "$repo" ]; then die 'session belongs to another checkout'; fi
|
|
412
1000
|
if [ -d "$backup" ]; then
|
|
413
|
-
if ! cmp -s "$
|
|
414
|
-
{ [ -f "$backup/yarn.lock" ] && ! cmp -s "$
|
|
415
|
-
{ [ ! -f "$backup/yarn.lock" ] && [ -e "$
|
|
1001
|
+
if ! cmp -s "$dir/package.json" "$backup/expected-package.json" ||
|
|
1002
|
+
{ [ -f "$backup/yarn.lock" ] && ! cmp -s "$dir/yarn.lock" "$backup/yarn.lock"; } ||
|
|
1003
|
+
{ [ ! -f "$backup/yarn.lock" ] && [ -e "$dir/yarn.lock" ]; }; then
|
|
416
1004
|
need node
|
|
417
|
-
node "$(dirname "$SELF")/npm-restore-check.cjs" "$
|
|
1005
|
+
node "$(dirname "$SELF")/npm-restore-check.cjs" "$dir" "$backup" ||
|
|
418
1006
|
die "npm files changed beyond the overlay; review $backup (nothing restored)"
|
|
419
1007
|
fi
|
|
420
|
-
mutate cp -p "$backup/package.json" "$
|
|
421
|
-
if [ -f "$backup/yarn.lock" ]; then mutate cp -p "$backup/yarn.lock" "$
|
|
1008
|
+
mutate cp -p "$backup/package.json" "$dir/package.json"
|
|
1009
|
+
if [ -f "$backup/yarn.lock" ]; then mutate cp -p "$backup/yarn.lock" "$dir/yarn.lock"; else mutate rm -f -- "$dir/yarn.lock"; fi
|
|
422
1010
|
mutate rm -rf -- "$backup"
|
|
423
1011
|
fi
|
|
424
|
-
mutate rm -f -- "$session/Directory.Packages.props" "$session/nuget.config" "$session/env" "$session/stamp"
|
|
1012
|
+
mutate rm -f -- "$session/Directory.Packages.props" "$session/nuget.config" "$session/env" "$session/stamp" "$session/npm-dir"
|
|
425
1013
|
if [ "$DRY" -eq 0 ]; then check "$repo"; fi
|
|
426
1014
|
}
|
|
427
1015
|
|
|
@@ -468,6 +1056,7 @@ scan_lines() {
|
|
|
468
1056
|
{
|
|
469
1057
|
number=$0; sub(/:.*/,"",number); line=$0; sub(/^[^:]*:/,"",line)
|
|
470
1058
|
if (line ~ /-local\.[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]\.g[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]/) hit("local stamp")
|
|
1059
|
+
if (line ~ /-train\.g[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]/) hit("train stamp")
|
|
471
1060
|
if (index(line,feed)) hit("local feed path")
|
|
472
1061
|
if (index(line,"DirectoryPackages" "PropsPath")) hit("CPM overlay property")
|
|
473
1062
|
if (index(line,"RestoreAdditional" "ProjectSources")) hit("additional restore sources")
|
|
@@ -496,10 +1085,10 @@ bounded_docker() {
|
|
|
496
1085
|
return "$status"
|
|
497
1086
|
}
|
|
498
1087
|
local_artifact() {
|
|
499
|
-
[[ "$1" =~ -local\.[0-9]{12}\.g[0-9a-f]{7}($|[^0-9A-Za-z]) ]]
|
|
1088
|
+
[[ "$1" =~ -(local\.[0-9]{12}|train)\.g[0-9a-f]{7}($|[^0-9A-Za-z]) ]]
|
|
500
1089
|
}
|
|
501
1090
|
clean() {
|
|
502
|
-
local version='' apply=0 consumer='' arg path cache image session maximum count=0
|
|
1091
|
+
local version='' apply=0 consumer='' arg path cache image session maximum count=0 rel=''
|
|
503
1092
|
local subjects=() kinds=()
|
|
504
1093
|
while [ $# -gt 0 ]; do
|
|
505
1094
|
arg=$1; shift
|
|
@@ -507,6 +1096,7 @@ clean() {
|
|
|
507
1096
|
--stamp) [ $# -gt 0 ] || die '--stamp requires a stamp'; [ -z "$version" ] || die 'choose --stamp or --all'; version=$1; shift; valid_stamp "$version" ;;
|
|
508
1097
|
--all) [ -z "$version" ] || die 'choose --stamp or --all'; version=all ;;
|
|
509
1098
|
--apply) apply=1 ;;
|
|
1099
|
+
--dir) [ $# -gt 0 ] || die '--dir requires a relative directory'; rel=${1%/}; shift ;;
|
|
510
1100
|
-*) die "unknown clean option: $arg" ;;
|
|
511
1101
|
*) [ -z "$consumer" ] || die 'clean takes at most one consumer'; consumer=$(repo_root "$arg") ;;
|
|
512
1102
|
esac
|
|
@@ -522,12 +1112,12 @@ clean() {
|
|
|
522
1112
|
local_artifact "${path##*/}" || continue
|
|
523
1113
|
if [ -n "$version" ] && [ "$version" != all ]; then case "$path" in *"$version"*) ;; *) continue ;; esac; fi
|
|
524
1114
|
subjects+=("$path"); kinds+=(file)
|
|
525
|
-
done < <(find "$cache" -type f \( -name '*.nupkg' -o -name '*.tgz' \) -print0)
|
|
1115
|
+
done < <(find "$cache" -type f \( -name '*.nupkg' -o -name '*.snupkg' -o -name '*.tgz' \) -print0)
|
|
526
1116
|
done
|
|
527
1117
|
if [ -d "$NUGET_CACHE" ]; then
|
|
528
1118
|
for cache in "$NUGET_CACHE"/*; do
|
|
529
1119
|
[ -d "$cache" ] && [ ! -L "$cache" ] || continue
|
|
530
|
-
for path in "$cache"/*-local.*; do
|
|
1120
|
+
for path in "$cache"/*-local.* "$cache"/*-train.*; do
|
|
531
1121
|
[ -d "$path" ] && [ ! -L "$path" ] || continue
|
|
532
1122
|
local_artifact "${path##*/}" || continue
|
|
533
1123
|
if [ -n "$version" ] && [ "$version" != all ] && [ "${path##*/}" != "$version" ]; then continue; fi
|
|
@@ -535,14 +1125,16 @@ clean() {
|
|
|
535
1125
|
done
|
|
536
1126
|
done
|
|
537
1127
|
fi
|
|
538
|
-
if
|
|
539
|
-
|
|
1128
|
+
if [ -n "$consumer" ] && [ ! -f "$consumer${rel:+/$rel}/package.json" ]; then
|
|
1129
|
+
printf 'SKIP: no package.json in %s; pass --dir REL for a Yarn project in a subdirectory\n' "$consumer${rel:+/$rel}" >&2
|
|
1130
|
+
elif command -v yarn >/dev/null 2>&1 && [ -n "$consumer" ]; then
|
|
1131
|
+
cache=$(cd "$consumer${rel:+/$rel}" && yarn config get cacheFolder) || die 'cannot discover Yarn cache'
|
|
540
1132
|
if [ -d "$cache" ]; then
|
|
541
1133
|
while IFS= read -r -d '' path; do
|
|
542
1134
|
local_artifact "${path##*/}" || continue
|
|
543
1135
|
if [ -n "$version" ] && [ "$version" != all ]; then case "$path" in *"$version"*) ;; *) continue ;; esac; fi
|
|
544
1136
|
subjects+=("$path"); kinds+=(file)
|
|
545
|
-
done < <(find "$cache" -type f -name '*-local.*' -print0)
|
|
1137
|
+
done < <(find "$cache" -type f \( -name '*-local.*' -o -name '*-train.*' \) -print0)
|
|
546
1138
|
fi
|
|
547
1139
|
else
|
|
548
1140
|
printf 'SKIP: Yarn cache discovery needs yarn and an explicit consumer\n' >&2
|
|
@@ -585,7 +1177,7 @@ clean() {
|
|
|
585
1177
|
session)
|
|
586
1178
|
[ -f "$path/stamp" ] && [ ! -L "$path" ] && [ ! -e "$path/npm-backup" ] || die "session changed: $path"
|
|
587
1179
|
validate_session "$path"
|
|
588
|
-
rm -f -- "$path/repo" "$path/stamp" "$path/env" "$path/Directory.Packages.props" "$path/nuget.config"
|
|
1180
|
+
rm -f -- "$path/repo" "$path/stamp" "$path/env" "$path/npm-dir" "$path/Directory.Packages.props" "$path/nuget.config"
|
|
589
1181
|
rmdir -- "$path" ;;
|
|
590
1182
|
esac
|
|
591
1183
|
done
|
|
@@ -593,14 +1185,17 @@ clean() {
|
|
|
593
1185
|
else printf 'dry-run: %s subjects (use --apply to remove)\n' "$count"; fi
|
|
594
1186
|
}
|
|
595
1187
|
|
|
1188
|
+
# shellcheck disable=SC2016 # fixtures assert literal $STAMP in printed commands
|
|
596
1189
|
self_test() {
|
|
597
1190
|
[ $# -eq 0 ] || die '--self-test takes no arguments'
|
|
598
|
-
local root home repo feed version output status passed=0 skipped=0 test_key arg
|
|
1191
|
+
local root home repo feed version output status passed=0 skipped=0 test_key arg map
|
|
599
1192
|
scratch; root=$TMP/self-test; mkdir -p "$root/home" "$root/repo"
|
|
600
|
-
home=$root/home; repo=$root/repo; feed=$home/.cache/cratis/local-feed
|
|
1193
|
+
map=$root/map.tsv; home=$root/home; repo=$root/repo; feed=$home/.cache/cratis/local-feed
|
|
601
1194
|
# System-only tests stay hermetic; npm tests inherit PATH to discover node.
|
|
602
1195
|
# Both isolate HOME/config and omit the host adapter.
|
|
603
1196
|
st() { env -i HOME="$home" PATH=/usr/bin:/bin:/usr/sbin:/sbin TMPDIR="$TMP" /bin/bash "$SELF" "$@"; }
|
|
1197
|
+
stm() { env -i HOME="$home" PATH=/usr/bin:/bin:/usr/sbin:/sbin TMPDIR="$TMP" CRATIS_LOCAL_MAP="$map" /bin/bash "$SELF" "$@"; }
|
|
1198
|
+
has() { grep -Fq -- "$1" "$2"; }
|
|
604
1199
|
st_npm() { env -i HOME="$home" PATH="$PATH" TMPDIR="$TMP" /bin/bash "$SELF" "$@"; }
|
|
605
1200
|
git -C "$repo" init -q
|
|
606
1201
|
repo=$(repo_root "$repo")
|
|
@@ -658,7 +1253,13 @@ LOCK
|
|
|
658
1253
|
grep -Fq "Import Project=\"$repo/Directory.Packages.props\"" "$output/Directory.Packages.props" || failed 'self-test: overlay import'
|
|
659
1254
|
grep -Fq "PackageVersion Update=\"Cratis.Test\" Version=\"$version\"" "$output/Directory.Packages.props" || failed 'self-test: pinned package Update'
|
|
660
1255
|
grep -Fq "PackageVersion Include=\"Cratis.Transitive\" Version=\"$version\"" "$output/Directory.Packages.props" || failed 'self-test: transitive package Include'
|
|
661
|
-
grep -Fq 'CentralPackageTransitivePinningEnabled
|
|
1256
|
+
if grep -Fq 'CentralPackageTransitivePinningEnabled' "$output/Directory.Packages.props"; then failed 'self-test: transitive pinning must be opt-in'; fi
|
|
1257
|
+
st use-nuget "$repo" "$version" --pin-transitive > "$root/output"
|
|
1258
|
+
grep -Fq 'CentralPackageTransitivePinningEnabled>true' "$output/Directory.Packages.props" || failed 'self-test: --pin-transitive enables pinning'
|
|
1259
|
+
grep -Fq "PackageVersion Include=\"Cratis.Transitive\" Version=\"$version\"" "$output/Directory.Packages.props" || failed 'self-test: --pin-transitive keeps the IDs'
|
|
1260
|
+
st use-nuget "$repo" "$version" Cratis.Test --pin-transitive > "$root/output" || failed 'self-test: --pin-transitive with explicit IDs'
|
|
1261
|
+
st use-nuget "$repo" "$version" > "$root/output"
|
|
1262
|
+
passed=$((passed + 3))
|
|
662
1263
|
test_key=DirectoryPackages"PropsPath"
|
|
663
1264
|
grep -Fq "export $test_key=" "$output/env" || failed 'self-test: env property'
|
|
664
1265
|
passed=$((passed + 5))
|
|
@@ -693,8 +1294,27 @@ LOCK
|
|
|
693
1294
|
st_npm use-npm "$repo" "$version"
|
|
694
1295
|
grep -Fq '"@cratis/transitive": "file:' "$repo/package.json" || failed 'self-test: transitive npm resolution'
|
|
695
1296
|
grep -Fq '"@cratis/test": "file:' "$repo/package.json" || failed 'self-test: direct npm resolution'
|
|
696
|
-
|
|
697
|
-
|
|
1297
|
+
# Execute the consumer tail printed by closure, with a fixture installation between
|
|
1298
|
+
# overlay and verification. In particular, check must not run before restore.
|
|
1299
|
+
{
|
|
1300
|
+
tsv pack Lib npm .
|
|
1301
|
+
tsv ids Lib npm @cratis/test,@cratis/transitive
|
|
1302
|
+
tsv uses App Lib npm .
|
|
1303
|
+
} > "$root/npm-closure.tsv"
|
|
1304
|
+
st_npm closure --map "$root/npm-closure.tsv" App="$repo" Lib="$repo" > "$root/npm-closure.out"
|
|
1305
|
+
printf 'node_modules/\n' >> "$repo/.git/info/exclude"
|
|
1306
|
+
for arg in test transitive; do
|
|
1307
|
+
mkdir -p "$repo/node_modules/@cratis/$arg"
|
|
1308
|
+
tar -xzf "$feed/npm/@cratis-$arg-$version.tgz" -C "$repo/node_modules/@cratis/$arg" --strip-components=1
|
|
1309
|
+
done
|
|
1310
|
+
status=0; st_npm check "$repo" > "$root/output" || status=$?
|
|
1311
|
+
[ "$status" -eq 1 ] || failed 'self-test: npm overlay needs restore before check'
|
|
1312
|
+
awk '/ verify-assets / { consumer=1 } consumer' "$root/npm-closure.out" > "$root/npm-closure-tail.sh"
|
|
1313
|
+
env -i HOME="$home" PATH="$PATH" TMPDIR="$TMP" STAMP="$version" \
|
|
1314
|
+
/bin/bash -e "$root/npm-closure-tail.sh" > "$root/output" || failed 'self-test: npm closure verification, restore, check and clean'
|
|
1315
|
+
cmp -s "$repo/package.json" <(git -C "$repo" show HEAD:package.json) || failed 'self-test: npm closure restores tracked manifest'
|
|
1316
|
+
grep -Fq 'dry-run:' "$root/output" || failed 'self-test: npm closure reaches clean'
|
|
1317
|
+
passed=$((passed + 6))
|
|
698
1318
|
printf '{"private":true,"dependencies":{"@cratis/test":"1.2.3"},"userSetting":true}\n' > "$repo/package.json"
|
|
699
1319
|
cp "$repo/package.json" "$root/original-package.json"
|
|
700
1320
|
cp "$repo/yarn.lock" "$root/original-yarn.lock"
|
|
@@ -741,6 +1361,95 @@ NODE
|
|
|
741
1361
|
passed=$((passed + 3))
|
|
742
1362
|
git -C "$repo" checkout -- package.json
|
|
743
1363
|
else printf 'SKIP: npm mutation/restore needs node on inherited PATH\n'; skipped=$((skipped + 10)); fi
|
|
1364
|
+
# pack-npm uses npm pack so a nested, gitignored dist directory listed in files is packed whole.
|
|
1365
|
+
if command -v npm >/dev/null 2>&1 && command -v node >/dev/null 2>&1; then
|
|
1366
|
+
local pk_dir=$root/npm-workspace pk_feed=$root/npm-feed pk_ver=1.2.3-local.202601010000.gabcdef0 pk_count
|
|
1367
|
+
mkdir -p "$pk_dir/dist/bundles/sub" "$pk_feed/npm"
|
|
1368
|
+
printf '{"name":"@cratis/nested","version":"%s","files":["dist/bundles","dist/**/*.d.ts"]}\n' "$pk_ver" > "$pk_dir/package.json"
|
|
1369
|
+
printf 'dist\n' > "$pk_dir/.gitignore"
|
|
1370
|
+
printf 'a\n' > "$pk_dir/dist/bundles/a.js"; printf 'b\n' > "$pk_dir/dist/bundles/sub/b.js"
|
|
1371
|
+
printf 'export {}\n' > "$pk_dir/dist/a.d.ts"
|
|
1372
|
+
git -C "$pk_dir" init -q
|
|
1373
|
+
(FEED=$pk_feed; pack_npm_workspace "$pk_dir" package.json "$pk_ver") >/dev/null || failed 'self-test: npm workspace pack'
|
|
1374
|
+
[ -f "$pk_feed/npm/@cratis-nested-$pk_ver.tgz" ] || failed 'self-test: npm tarball name'
|
|
1375
|
+
pk_count=$(tar tzf "$pk_feed/npm/@cratis-nested-$pk_ver.tgz" | grep -c 'dist/bundles/') || true
|
|
1376
|
+
[ "$pk_count" -eq 2 ] || failed "self-test: nested dist files packed ($pk_count of 2)"
|
|
1377
|
+
tar tzf "$pk_feed/npm/@cratis-nested-$pk_ver.tgz" | grep -Fq 'dist/a.d.ts' || failed 'self-test: declaration glob packed'
|
|
1378
|
+
printf '{"name":"plainpkg","version":"%s","files":["dist/bundles"]}\n' "$pk_ver" > "$pk_dir/package.json"
|
|
1379
|
+
(FEED=$pk_feed; pack_npm_workspace "$pk_dir" package.json "$pk_ver") >/dev/null || failed 'self-test: unscoped npm workspace pack'
|
|
1380
|
+
[ -f "$pk_feed/npm/plainpkg-$pk_ver.tgz" ] || failed 'self-test: unscoped npm tarball kept'
|
|
1381
|
+
passed=$((passed + 5))
|
|
1382
|
+
else printf 'SKIP: npm pack needs node and npm on inherited PATH\n'; skipped=$((skipped + 5)); fi
|
|
1383
|
+
# Multi-repo stamp: the highest next patch wins and every HEAD is reported.
|
|
1384
|
+
local repo2 v2
|
|
1385
|
+
repo2=$root/repo2; mkdir -p "$repo2"; git -C "$repo2" init -q
|
|
1386
|
+
git -C "$repo2" config user.email test@example.invalid; git -C "$repo2" config user.name 'Local package self-test'
|
|
1387
|
+
printf 'x\n' > "$repo2/f"; git -C "$repo2" add f; git -C "$repo2" commit -qm initial; git -C "$repo2" tag v2.0.0
|
|
1388
|
+
v2=$(st stamp "$repo" "$repo2" 2> "$root/stderr")
|
|
1389
|
+
[[ "$v2" =~ ^2\.0\.1-local\.[0-9]{12}\.g$(git -C "$repo2" rev-parse --short=7 HEAD)$ ]] || failed 'self-test: multi-repo stamp uses highest base and last sha'
|
|
1390
|
+
[ "$(grep -c '^HEAD ' "$root/stderr")" -eq 2 ] || failed 'self-test: multi-repo stamp prints each HEAD'
|
|
1391
|
+
v2=$(st stamp "$repo2" "$repo" 2> /dev/null)
|
|
1392
|
+
[[ "$v2" =~ ^2\.0\.1-local\. ]] || failed 'self-test: multi-repo stamp order independent base'
|
|
1393
|
+
passed=$((passed + 3))
|
|
1394
|
+
# A train stamp covers every producer: changing a non-last producer changes it, order does not.
|
|
1395
|
+
local ta tb tc
|
|
1396
|
+
ta=$(st stamp --train "$repo" "$repo2" 2> /dev/null); tb=$(st stamp --train "$repo2" "$repo" 2> /dev/null)
|
|
1397
|
+
[ "$ta" = "$tb" ] || failed 'self-test: train stamp is independent of producer order'
|
|
1398
|
+
[[ "$ta" =~ ^2\.0\.1-train\.g[0-9a-f]{7}$ ]] || failed "self-test: multi-producer train grammar ($ta)"
|
|
1399
|
+
printf 'y\n' > "$repo/g"; git -C "$repo" add g; git -C "$repo" commit -qm second
|
|
1400
|
+
tc=$(st stamp --train "$repo" "$repo2" 2> /dev/null)
|
|
1401
|
+
[ "$tc" != "$ta" ] || failed 'self-test: a change in a non-last producer must change the train stamp'
|
|
1402
|
+
git -C "$repo" reset -q --hard HEAD~1
|
|
1403
|
+
[ "$(st stamp --train "$repo" "$repo2" 2> /dev/null)" = "$ta" ] || failed 'self-test: train stamp returns with the same state'
|
|
1404
|
+
passed=$((passed + 4))
|
|
1405
|
+
# Train mode: a deterministic X.Y.Z-train.g<sha7> stamp, accepted everywhere a local stamp is.
|
|
1406
|
+
local tv tv2 tsha tleak
|
|
1407
|
+
tsha=$(printf '%s %s\n' "$(basename "$repo")" "$(git -C "$repo" rev-parse HEAD)" | shasum -a 1 | cut -c1-7)
|
|
1408
|
+
tv=$(st stamp --train "$repo") || failed 'self-test: stamp --train'
|
|
1409
|
+
tv2=$(st stamp --train "$repo")
|
|
1410
|
+
[[ "$tv" =~ ^1\.2\.4-train\.g$tsha$ ]] || failed "self-test: train stamp grammar ($tv)"
|
|
1411
|
+
[ "$tv" = "$tv2" ] || failed 'self-test: train stamp is deterministic'
|
|
1412
|
+
: > "$feed/nuget/Cratis.Train.$tv.nupkg"; : > "$feed/nuget/Cratis.Train.1.2.4-local.200001010000.g1234567.nupkg"
|
|
1413
|
+
st --dry-run use-nuget "$repo" "$tv" Cratis.Train > "$root/output" || failed 'self-test: use-nuget accepts a train stamp'
|
|
1414
|
+
for tleak in 1.2.4-train.1234567 1.2.4-train.200001010000.g1234567 1.2.4-train.gABCDEF0 1.2.4-train.g123456; do
|
|
1415
|
+
status=0; st --dry-run use-nuget "$repo" "$tleak" Cratis.Train > "$root/output" 2>&1 || status=$?
|
|
1416
|
+
[ "$status" -eq 2 ] || failed "self-test: invalid train stamp accepted ($tleak)"
|
|
1417
|
+
done
|
|
1418
|
+
st clean --stamp "$tv" > "$root/output" || failed 'self-test: clean accepts a train stamp'
|
|
1419
|
+
has "Cratis.Train.$tv.nupkg" "$root/output" || failed 'self-test: clean lists the train artifact'
|
|
1420
|
+
if has '200001010000' "$root/output"; then failed 'self-test: clean --stamp train must not list a local stamp'; fi
|
|
1421
|
+
rm "$feed/nuget/Cratis.Train.$tv.nupkg" "$feed/nuget/Cratis.Train.1.2.4-local.200001010000.g1234567.nupkg"
|
|
1422
|
+
printf 'Version="%s"\n' "$tv" > "$repo/probe-train"
|
|
1423
|
+
status=0; st check "$repo" > "$root/output" 2>&1 || status=$?
|
|
1424
|
+
[ "$status" -eq 1 ] || failed 'self-test: check misses an untracked train version'
|
|
1425
|
+
has 'train stamp' "$root/output" || failed 'self-test: check names the train stamp'
|
|
1426
|
+
git -C "$repo" add probe-train
|
|
1427
|
+
status=0; st check "$repo" > "$root/output" 2>&1 || status=$?
|
|
1428
|
+
[ "$status" -eq 1 ] || failed 'self-test: check misses a staged train version'
|
|
1429
|
+
status=0; st check "$repo" --staged > "$root/output" 2>&1 || status=$?
|
|
1430
|
+
[ "$status" -eq 1 ] || failed 'self-test: check --staged misses a train version'
|
|
1431
|
+
git -C "$repo" rm -qf --cached probe-train; rm "$repo/probe-train"
|
|
1432
|
+
st check "$repo" > "$root/output" || failed 'self-test: check clean after removing the train probe'
|
|
1433
|
+
passed=$((passed + 13))
|
|
1434
|
+
# use-npm --dir for a Yarn project in a subdirectory (no root package.json).
|
|
1435
|
+
if command -v node >/dev/null 2>&1; then
|
|
1436
|
+
local sub=$root/repo3
|
|
1437
|
+
mkdir -p "$sub/Source/Workbench"; git -C "$sub" init -q
|
|
1438
|
+
git -C "$sub" config user.email test@example.invalid; git -C "$sub" config user.name 'Local package self-test'
|
|
1439
|
+
cp "$root/original-package.json" "$sub/Source/Workbench/package.json" 2>/dev/null || printf '{"private":true,"dependencies":{"@cratis/test":"1.2.3"}}\n' > "$sub/Source/Workbench/package.json"
|
|
1440
|
+
printf '# lock\n' > "$sub/Source/Workbench/yarn.lock"
|
|
1441
|
+
git -C "$sub" add .; git -C "$sub" commit -qm initial
|
|
1442
|
+
status=0; st_npm use-npm "$sub" "$version" > "$root/output" 2>&1 || status=$?
|
|
1443
|
+
[ "$status" -eq 2 ] || failed 'self-test: use-npm without --dir needs a root package.json'
|
|
1444
|
+
st_npm use-npm "$sub" "$version" --dir Source/Workbench > "$root/output" || failed 'self-test: use-npm --dir'
|
|
1445
|
+
grep -Fq '"@cratis/test": "file:' "$sub/Source/Workbench/package.json" || failed 'self-test: use-npm --dir overlays the subdirectory manifest'
|
|
1446
|
+
status=0; st_npm use-npm "$sub" "$version" --dir ../x > "$root/output" 2>&1 || status=$?
|
|
1447
|
+
[ "$status" -eq 2 ] || failed 'self-test: use-npm --dir rejects escapes'
|
|
1448
|
+
st_npm restore "$sub" > "$root/output" || failed 'self-test: restore follows --dir'
|
|
1449
|
+
git -C "$sub" diff --quiet || failed 'self-test: restore --dir leaves the tracked files clean'
|
|
1450
|
+
[ ! -e "$(git -C "$sub" rev-parse --absolute-git-dir)/cratis-local/npm-dir" ] || failed 'self-test: restore removes npm-dir'
|
|
1451
|
+
passed=$((passed + 6))
|
|
1452
|
+
else printf 'SKIP: use-npm --dir needs node\n'; skipped=$((skipped + 6)); fi
|
|
744
1453
|
mkdir -p "$home/.nuget/packages/cratis.test/$version" "$home/.nuget/packages/cratis.test/1.2.3"
|
|
745
1454
|
mkdir -p "$home/.nuget/packages/cratis.test/1.2.3-local.keep"
|
|
746
1455
|
for arg in nuget npm; do
|
|
@@ -756,16 +1465,312 @@ NODE
|
|
|
756
1465
|
st clean --stamp "$version" > "$root/output"
|
|
757
1466
|
grep -Fq "$home/.nuget/packages/cratis.test/$version" "$root/output" || failed 'self-test: clean lists local cache'
|
|
758
1467
|
[ -d "$home/.nuget/packages/cratis.test/$version" ] || failed 'self-test: dry-run mutated cache'
|
|
1468
|
+
: > "$feed/nuget/Cratis.Test.$version.snupkg"
|
|
759
1469
|
st clean --stamp "$version" --apply > "$root/output"
|
|
1470
|
+
[ ! -e "$feed/nuget/Cratis.Test.$version.snupkg" ] || failed 'self-test: clean left a symbols package'
|
|
760
1471
|
[ ! -e "$home/.nuget/packages/cratis.test/$version" ] || failed 'self-test: clean failed to remove cache'
|
|
761
1472
|
[ -d "$home/.nuget/packages/cratis.test/1.2.3" ] || failed 'self-test: clean removed real version'
|
|
762
|
-
passed=$((passed +
|
|
1473
|
+
passed=$((passed + 5))
|
|
763
1474
|
printf 'SKIP: real dotnet/Yarn/Docker packing is not a hermetic self-test\n'; skipped=$((skipped + 3))
|
|
1475
|
+
self_test_matrix
|
|
764
1476
|
printf 'self-test: %s passed, %s skipped\n' "$passed" "$skipped"
|
|
765
1477
|
}
|
|
766
1478
|
|
|
1479
|
+
# C fixtures: the printed closure is one plan, in order, and every line is valid shell.
|
|
1480
|
+
self_test_closure() {
|
|
1481
|
+
local name line a b c p last first fa fl fm
|
|
1482
|
+
for name in App Lib Mid; do
|
|
1483
|
+
mkdir -p "$root/fx-$name"; git -C "$root/fx-$name" init -q
|
|
1484
|
+
git -C "$root/fx-$name" config user.email test@example.invalid; git -C "$root/fx-$name" config user.name 'Local package self-test'
|
|
1485
|
+
git -C "$root/fx-$name" commit -q --allow-empty -m initial
|
|
1486
|
+
done
|
|
1487
|
+
fa=$(git -C "$root/fx-App" rev-parse --show-toplevel); fl=$(git -C "$root/fx-Lib" rev-parse --show-toplevel); fm=$(git -C "$root/fx-Mid" rev-parse --show-toplevel)
|
|
1488
|
+
expect_status 0 'C1 closure for the fixture plan' fxm closure App="$root/fx-App" Lib="$root/fx-Lib" Mid="$root/fx-Mid"
|
|
1489
|
+
cp "$root/output" "$root/closure.out"
|
|
1490
|
+
bash -n "$root/closure.out" || failed 'self-test: C2 every printed closure line must be valid shell'
|
|
1491
|
+
[ "$(grep -c ' verify-pack ' "$root/closure.out")" -eq 1 ] || failed 'self-test: C1 exactly one verify-pack'
|
|
1492
|
+
a=$(grep -n ' verify-pack ' "$root/closure.out" | cut -d: -f1)
|
|
1493
|
+
last=$(grep -nE ' pack-(nuget|npm) ' "$root/closure.out" | tail -n 1 | cut -d: -f1)
|
|
1494
|
+
first=$(grep -n " use-nuget $fa " "$root/closure.out" | head -n 1 | cut -d: -f1)
|
|
1495
|
+
{ [ "$a" -gt "$last" ] && [ "$a" -lt "$first" ]; } || failed 'self-test: C1 verify-pack sits after the last pack and before the consumer overlay'
|
|
1496
|
+
line=$(grep ' verify-pack ' "$root/closure.out")
|
|
1497
|
+
case "$line" in *' --consumer App --producer Lib --producer Mid'*) ;; *) failed 'self-test: C1 verify-pack carries the whole plan' ;; esac
|
|
1498
|
+
grep -q "pack-nuget . \"\$STAMP\" --property A=1 --property B=2 Lib.Extra.slnx" "$root/closure.out" || failed 'self-test: C2 the second Lib row carries both properties'
|
|
1499
|
+
[ "$(grep -c "pack-nuget \. \"\$STAMP\" Lib.slnx" "$root/closure.out")" -eq 1 ] || failed 'self-test: C2 the first Lib row carries no properties'
|
|
1500
|
+
# C4: Mid's overlay, then its pack in a shell that sources the env file, then its restore.
|
|
1501
|
+
a=$(grep -n " use-nuget $fm " "$root/closure.out" | cut -d: -f1)
|
|
1502
|
+
b=$(grep -n "$fm && \. .*/env && .* pack-nuget " "$root/closure.out" | cut -d: -f1)
|
|
1503
|
+
c=$(grep -n " restore $fm" "$root/closure.out" | cut -d: -f1)
|
|
1504
|
+
{ [ -n "$a" ] && [ -n "$b" ] && [ -n "$c" ] && [ "$a" -lt "$b" ] && [ "$b" -lt "$c" ]; } || failed 'self-test: C4 overlay, sourced pack and restore are in order'
|
|
1505
|
+
if grep -q " use-nuget $fl " "$root/closure.out"; then failed 'self-test: C4 Lib has no upstream to overlay'; fi
|
|
1506
|
+
passed=$((passed + 6))
|
|
1507
|
+
# C2 (real map): Chronicle's three pack properties are separate flags; C3: an invalid plan prints nothing.
|
|
1508
|
+
expect_status 0 'C2 real closure Studio <- Chronicle' st closure Studio="$root/fx-App" Chronicle="$root/fx-Lib"
|
|
1509
|
+
cp "$root/output" "$root/closure.out"
|
|
1510
|
+
bash -n "$root/closure.out" || failed 'self-test: C2 real closure is valid shell'
|
|
1511
|
+
p=$(grep 'pack-nuget' "$root/closure.out" | grep -o -- '--property' | wc -l | tr -d ' ')
|
|
1512
|
+
[ "$p" -eq 3 ] || failed "self-test: C2 Chronicle carries 3 --property flags (got $p)"
|
|
1513
|
+
expect_status 2 'C3 closure for an unplannable pair prints nothing' st closure Stage="$root/fx-App" Screenplay="$root/fx-Lib"
|
|
1514
|
+
[ ! -s "$root/output" ] || failed 'self-test: C3 stdout must be empty'
|
|
1515
|
+
expect_status 0 'closure --train' st closure --train Studio="$root/fx-App" Chronicle="$root/fx-Lib"
|
|
1516
|
+
has 'stamp --train' "$root/output" || failed 'self-test: closure --train prints a train stamp'
|
|
1517
|
+
passed=$((passed + 3))
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
# V fixtures: verify-pack and verify-assets against a fixture feed (needs zip, unzip and tar).
|
|
1521
|
+
# Each fixture starts from the same complete feed so one case cannot hide behind another.
|
|
1522
|
+
self_test_verify() {
|
|
1523
|
+
local vfeed=$root/vfeed s=1.2.4-local.202601010000.gaaaaaaa old=1.2.4-local.200001010000.g1234567 args id
|
|
1524
|
+
local lib=(--consumer App --producer Lib --producer Mid)
|
|
1525
|
+
local nodedir; nodedir=$(dirname "$(command -v node)")
|
|
1526
|
+
fxv() { env -i HOME="$home" PATH="$nodedir:/usr/bin:/bin:/usr/sbin:/sbin" TMPDIR="$TMP" CRATIS_LOCAL_MAP="$fx" CRATIS_LOCAL_FEED="$vfeed" /bin/bash "$SELF" "$@"; }
|
|
1527
|
+
if ! command -v zip >/dev/null 2>&1 || ! command -v unzip >/dev/null 2>&1 || ! command -v tar >/dev/null 2>&1 || ! command -v node >/dev/null 2>&1; then
|
|
1528
|
+
printf 'SKIP: verify fixtures need zip, unzip, tar and node\n'; skipped=$((skipped + 30)); return 0
|
|
1529
|
+
fi
|
|
1530
|
+
# mkpkg ID STAMP [DEPENDENCY-ID RANGE]...: a .nupkg (a zip with a nuspec) or, for an @scope/name ID, an npm tarball.
|
|
1531
|
+
mkpkg() {
|
|
1532
|
+
local pid=$1 stamp=$2 deps='' dir=$root/pk
|
|
1533
|
+
shift 2
|
|
1534
|
+
mkdir -p "$dir" "$vfeed/nuget" "$vfeed/npm"
|
|
1535
|
+
case "$pid" in
|
|
1536
|
+
@*) mkdir -p "$dir/package"; printf '{"name":"%s","version":"%s"}\n' "$pid" "$stamp" > "$dir/package/package.json"
|
|
1537
|
+
(cd "$dir" && tar -czf "$vfeed/npm/${pid//\//-}-$stamp.tgz" package) ;;
|
|
1538
|
+
*) while [ $# -ge 2 ]; do deps="$deps<dependency id=\"$1\" version=\"$2\" />"; shift 2; done
|
|
1539
|
+
printf '<package><metadata><id>%s</id><dependencies>%s</dependencies></metadata></package>\n' "$pid" "$deps" > "$dir/$pid.nuspec"
|
|
1540
|
+
rm -f "$vfeed/nuget/$pid.$stamp.nupkg"; (cd "$dir" && zip -q "$vfeed/nuget/$pid.$stamp.nupkg" "$pid.nuspec") ;;
|
|
1541
|
+
esac
|
|
1542
|
+
}
|
|
1543
|
+
# mkfeed STAMP [MID-DEPENDENCY-RANGE]: the complete feed of the plan "App Lib Mid".
|
|
1544
|
+
mkfeed() {
|
|
1545
|
+
rm -rf "$vfeed"
|
|
1546
|
+
mkpkg Cratis.Lib "$1"; mkpkg Cratis.Lib.Core "$1"; mkpkg @cratis/lib "$1"
|
|
1547
|
+
mkpkg Cratis.Mid "$1" Cratis.Lib "${2:-$1}"
|
|
1548
|
+
mkpkg Cratis.Extra "$1"
|
|
1549
|
+
}
|
|
1550
|
+
# mkassets DIR ID/VERSION...: a restore result that resolves exactly these packages.
|
|
1551
|
+
mkassets() {
|
|
1552
|
+
local dir=$1 entry sep=''
|
|
1553
|
+
shift; mkdir -p "$dir/obj"
|
|
1554
|
+
{ printf '{\n "libraries": {\n'
|
|
1555
|
+
for entry in "$@"; do printf '%s "%s": {\n "type": "package"\n }' "$sep" "$entry"; sep=$',\n'; done
|
|
1556
|
+
printf '\n }\n}\n'; } > "$dir/obj/project.assets.json"
|
|
1557
|
+
}
|
|
1558
|
+
# V1: complete feed, plan-mode verify and the exact JSON line.
|
|
1559
|
+
mkfeed "$s"
|
|
1560
|
+
expect_status 0 'V1 complete feed verifies' fxv verify-pack "$s" "${lib[@]}"
|
|
1561
|
+
has 'verify-pack: 4 planned artifacts present' "$root/output" || failed 'self-test: V1 summary'
|
|
1562
|
+
fxv verify-pack "$s" "${lib[@]}" --json > "$root/output" 2> "$root/stderr" || failed 'self-test: V1 json'
|
|
1563
|
+
printf '{"stamp":"%s","packages":[{"id":"Cratis.Lib","version":"%s"},{"id":"Cratis.Lib.Core","version":"%s"},{"id":"@cratis/lib","version":"%s"},{"id":"Cratis.Mid","version":"%s"}]}\n' "$s" "$s" "$s" "$s" "$s" > "$root/json.expected"
|
|
1564
|
+
cmp -s "$root/output" "$root/json.expected" || failed 'self-test: V1 json line grammar'
|
|
1565
|
+
passed=$((passed + 2))
|
|
1566
|
+
# V2: a cross-producer dependency at the wrong version.
|
|
1567
|
+
mkfeed "$s" 1.0.0
|
|
1568
|
+
expect_status 1 'V2 wrong-version cross-producer dependency' fxv verify-pack "$s" "${lib[@]}"
|
|
1569
|
+
has 'dependency: Cratis.Mid -> Cratis.Lib 1.0.0' "$root/output" || failed 'self-test: V2 names the dependency'
|
|
1570
|
+
passed=$((passed + 1))
|
|
1571
|
+
# V3: lower bound equal to the stamp passes; a range that merely contains the stamp does not.
|
|
1572
|
+
for args in "[$s, )" "$s" "[$s]"; do
|
|
1573
|
+
mkfeed "$s" "$args"
|
|
1574
|
+
expect_status 0 "V3 dependency range $args" fxv verify-pack "$s" "${lib[@]}"
|
|
1575
|
+
done
|
|
1576
|
+
mkfeed "$s" "[1.0.0, $s)"
|
|
1577
|
+
expect_status 1 'V3 range with the stamp as upper bound' fxv verify-pack "$s" "${lib[@]}"
|
|
1578
|
+
# V3b: strict nuspec parsing. mkraw ID STAMP DEPENDENCIES-XML writes the dependencies element verbatim.
|
|
1579
|
+
mkraw() {
|
|
1580
|
+
local pk=$root/pk; mkdir -p "$pk"
|
|
1581
|
+
printf '<package><metadata><id>%s</id><dependencies>%s</dependencies></metadata></package>\n' "$1" "$3" > "$pk/$1.nuspec"
|
|
1582
|
+
rm -f "$vfeed/nuget/$1.$2.nupkg"; (cd "$pk" && zip -q "$vfeed/nuget/$1.$2.nupkg" "$1.nuspec")
|
|
1583
|
+
}
|
|
1584
|
+
mkfeed "$s"; mkraw Cratis.Mid "$s" "<dependency version=\"1.0.0\" id=\"Cratis.Lib\" />"
|
|
1585
|
+
expect_status 1 'V3b reversed attributes are read' fxv verify-pack "$s" "${lib[@]}"
|
|
1586
|
+
has 'dependency: Cratis.Mid -> Cratis.Lib 1.0.0' "$root/output" || failed 'self-test: V3b reversed attributes name the dependency'
|
|
1587
|
+
mkfeed "$s"; mkraw Cratis.Mid "$s" "<dependency version='1.0.0' id='Cratis.Lib' />"
|
|
1588
|
+
expect_status 1 'V3b single quotes are read' fxv verify-pack "$s" "${lib[@]}"
|
|
1589
|
+
mkfeed "$s"; mkraw Cratis.Mid "$s" "<dependency
|
|
1590
|
+
id=\"Cratis.Lib\"
|
|
1591
|
+
version=\"1.0.0\"
|
|
1592
|
+
exclude=\"Build\" />"
|
|
1593
|
+
expect_status 1 'V3b multiline element is read' fxv verify-pack "$s" "${lib[@]}"
|
|
1594
|
+
mkfeed "$s"; mkraw Cratis.Mid "$s" "<dependency version='$s' id='Cratis.Lib' exclude='Build'/>"
|
|
1595
|
+
expect_status 0 'V3b reversed single-quoted element at the stamp passes' fxv verify-pack "$s" "${lib[@]}"
|
|
1596
|
+
for bad in "<dependency id=\"Cratis.Lib\" />" "<dependency version=\"$s\" />" "<dependency id=\"\" version=\"$s\" />" "<dependency id=\"Cratis.Lib\" version=\"$s\""; do
|
|
1597
|
+
mkfeed "$s"; mkraw Cratis.Mid "$s" "$bad"
|
|
1598
|
+
expect_status 1 "V3b malformed element fails: $bad" fxv verify-pack "$s" "${lib[@]}"
|
|
1599
|
+
has 'unparseable dependency: Cratis.Mid' "$root/output" || failed 'self-test: V3b malformed element is named'
|
|
1600
|
+
done
|
|
1601
|
+
for args in "($s,)" "($s, )" "[$s, $s]" "[$s,]" "[$s, 9.9.9)" "$s-x" "[ $s ]x"; do
|
|
1602
|
+
mkfeed "$s" "$args"
|
|
1603
|
+
expect_status 1 "V3b dependency range rejected: $args" fxv verify-pack "$s" "${lib[@]}"
|
|
1604
|
+
done
|
|
1605
|
+
# V4: a dependency outside the plan is ignored.
|
|
1606
|
+
mkfeed "$s"; mkpkg Cratis.Mid "$s" Cratis.Lib "$s" Cratis.Other 1.0.0
|
|
1607
|
+
expect_status 0 'V4 unplanned dependency is ignored' fxv verify-pack "$s" "${lib[@]}"
|
|
1608
|
+
# V5: a missing planned artifact is not satisfied by another stamp.
|
|
1609
|
+
mkfeed "$s"; rm "$vfeed/nuget/Cratis.Lib.Core.$s.nupkg"; mkpkg Cratis.Lib.Core "$old"
|
|
1610
|
+
expect_status 1 'V5 missing planned artifact' fxv verify-pack "$s" "${lib[@]}"
|
|
1611
|
+
has "missing: Cratis.Lib.Core $s (Lib nuget)" "$root/output" || failed 'self-test: V5 names the artifact'
|
|
1612
|
+
mkassets "$root/cons" "Cratis.Lib/$s" "Cratis.Lib.Core/$s" "Cratis.Mid/$s"
|
|
1613
|
+
expect_status 1 'V5 verify-assets plan mode also fails on the missing artifact' fxv verify-assets "$root/cons" "$s" "${lib[@]}"
|
|
1614
|
+
passed=$((passed + 2))
|
|
1615
|
+
# V6: a real NuGet-only plan never demands the npm tarballs of a dual-ecosystem producer.
|
|
1616
|
+
rm -rf "$vfeed"
|
|
1617
|
+
while IFS= read -r id; do mkpkg "$id" "$s"; done < <(awk -F'\t' '$1 == "ids" && $2 == "Arc" && $3 == "nuget" { n = split($4, a, ","); for (i = 1; i <= n; i++) print a[i] }' "$(dirname "$SELF")/dependency-map.tsv")
|
|
1618
|
+
expect_status 0 'V6 real Arc NuGet plan without npm tarballs' env -i HOME="$home" PATH=/usr/bin:/bin TMPDIR="$TMP" CRATIS_LOCAL_FEED="$vfeed" /bin/bash "$SELF" verify-pack "$s" --consumer Stage --producer Arc
|
|
1619
|
+
# V7: stale assets (an older local stamp) name the ID, version and path.
|
|
1620
|
+
mkfeed "$s"
|
|
1621
|
+
mkassets "$root/cons" "Cratis.Lib/$old" "Cratis.Lib.Core/$s" "Cratis.Mid/$s"
|
|
1622
|
+
expect_status 1 'V7 stale local stamp' fxv verify-assets "$root/cons" "$s" "${lib[@]}"
|
|
1623
|
+
has "Cratis.Lib $old $root/cons/obj/project.assets.json" "$root/output" || failed 'self-test: V7 names id, version and path'
|
|
1624
|
+
passed=$((passed + 1))
|
|
1625
|
+
# V8: a train stamp changes with a commit in a non-last producer, so the old feed is stale.
|
|
1626
|
+
local t1 t2
|
|
1627
|
+
t1=$(st stamp --train "$repo" "$repo2" 2> /dev/null); mkfeed "$t1"
|
|
1628
|
+
expect_status 0 'V8 train feed verifies at its own stamp' fxv verify-pack "$t1" "${lib[@]}"
|
|
1629
|
+
git -C "$repo" commit -q --allow-empty -m 'non-last producer change'
|
|
1630
|
+
t2=$(st stamp --train "$repo" "$repo2" 2> /dev/null)
|
|
1631
|
+
[ "$t1" != "$t2" ] || failed 'self-test: V8 stamp changes with a non-last producer commit'
|
|
1632
|
+
expect_status 1 'V8 stale train stamp' fxv verify-pack "$t2" "${lib[@]}"
|
|
1633
|
+
has "missing: Cratis.Lib $t2" "$root/output" || failed 'self-test: V8 missing at the new train stamp'
|
|
1634
|
+
passed=$((passed + 2))
|
|
1635
|
+
# V9: a second, stale installed copy of an npm package.
|
|
1636
|
+
mkfeed "$s"
|
|
1637
|
+
mkdir -p "$root/npmcons/node_modules/@cratis/lib"
|
|
1638
|
+
printf '{"name":"@cratis/lib","version":"%s"}\n' "$s" > "$root/npmcons/node_modules/@cratis/lib/package.json"
|
|
1639
|
+
expect_status 0 'V9 one installed copy at the stamp' fxv verify-assets "$root/npmcons" "$s" --npm "${lib[@]}"
|
|
1640
|
+
mkdir -p "$root/npmcons/node_modules/x/node_modules/@cratis/lib"
|
|
1641
|
+
printf '{"name":"@cratis/lib","version":"1.0.0"}\n' > "$root/npmcons/node_modules/x/node_modules/@cratis/lib/package.json"
|
|
1642
|
+
expect_status 1 'V9 stale second npm copy' fxv verify-assets "$root/npmcons" "$s" --npm "${lib[@]}"
|
|
1643
|
+
has "@cratis/lib 1.0.0" "$root/output" || failed 'self-test: V9 names the stale copy'
|
|
1644
|
+
expect_status 1 'V9 feed-wide npm mode flags the same copy' fxv verify-assets "$root/npmcons" "$s" --npm
|
|
1645
|
+
passed=$((passed + 1))
|
|
1646
|
+
# V10: a named producer the consumer never resolved.
|
|
1647
|
+
mkfeed "$s"
|
|
1648
|
+
mkassets "$root/cons" "Cratis.Mid/$s" "Cratis.Extra/$s"
|
|
1649
|
+
expect_status 1 'V10 unconsumed producer' fxv verify-assets "$root/cons" "$s" --consumer App --producer Lib --producer Mid --producer Extra
|
|
1650
|
+
has 'unconsumed: Lib (nuget)' "$root/output" || failed 'self-test: V10 names the unconsumed producer'
|
|
1651
|
+
mkassets "$root/cons" "Cratis.Mid/$s" "Cratis.Extra/$s" "Cratis.Lib/$s"
|
|
1652
|
+
expect_status 0 'V10 one consumed ID is enough' fxv verify-assets "$root/cons" "$s" --consumer App --producer Lib --producer Mid --producer Extra
|
|
1653
|
+
passed=$((passed + 1))
|
|
1654
|
+
# V11: an unrelated package at the stamp is ignored in plan mode and flagged by the feed-wide mode.
|
|
1655
|
+
mkfeed "$s"; mkpkg Cratis.Unrelated "$s"
|
|
1656
|
+
mkassets "$root/cons" "Cratis.Lib/$s" "Cratis.Mid/$s" "Cratis.Unrelated/9.9.9"
|
|
1657
|
+
expect_status 0 'V11 plan mode ignores an unrelated package' fxv verify-assets "$root/cons" "$s" "${lib[@]}"
|
|
1658
|
+
expect_status 1 'V11 feed-wide mode flags it' fxv verify-assets "$root/cons" "$s"
|
|
1659
|
+
# V12: --json lists only what the consumer resolved, whatever else the feed holds.
|
|
1660
|
+
mkassets "$root/cons" "Cratis.Lib/$s" "Cratis.Mid/$s"
|
|
1661
|
+
fxv verify-assets "$root/cons" "$s" "${lib[@]}" --json > "$root/output" 2> "$root/stderr" || failed 'self-test: V12 json'
|
|
1662
|
+
printf '{"stamp":"%s","packages":[{"id":"Cratis.Lib","version":"%s"},{"id":"Cratis.Mid","version":"%s"}]}\n' "$s" "$s" "$s" > "$root/json.expected"
|
|
1663
|
+
cmp -s "$root/output" "$root/json.expected" || failed 'self-test: V12 json lists only consumer resolutions'
|
|
1664
|
+
passed=$((passed + 1))
|
|
1665
|
+
# V15: unreadable evidence is a failure, never a skip: broken assets JSON, a package.json without a version,
|
|
1666
|
+
# and a tarball whose package.json has no name. A nested "version" key must not stand in for the top-level one.
|
|
1667
|
+
mkdir -p "$root/badassets/obj"; printf '{"libraries": {"Cratis.Lib/%s": {"type": "package"' "$s" > "$root/badassets/obj/project.assets.json"
|
|
1668
|
+
expect_status 2 'V15 truncated project.assets.json' fxv verify-assets "$root/badassets" "$s" "${lib[@]}"
|
|
1669
|
+
printf '{"version": 3}\n' > "$root/badassets/obj/project.assets.json"
|
|
1670
|
+
expect_status 2 'V15 assets without libraries' fxv verify-assets "$root/badassets" "$s" "${lib[@]}"
|
|
1671
|
+
mkfeed "$s"; rm -rf "$root/npmbad"; mkdir -p "$root/npmbad/node_modules/@cratis/lib"
|
|
1672
|
+
printf '{"name":"@cratis/lib"}\n' > "$root/npmbad/node_modules/@cratis/lib/package.json"
|
|
1673
|
+
expect_status 2 'V15 installed package.json without a version' fxv verify-assets "$root/npmbad" "$s" --npm "${lib[@]}"
|
|
1674
|
+
printf '{"name":"@cratis/lib","dependencies":{"x":{"version":"%s"}},"version":"1.0.0"}\n' "$s" > "$root/npmbad/node_modules/@cratis/lib/package.json"
|
|
1675
|
+
expect_status 1 'V15 nested version does not hide the real one' fxv verify-assets "$root/npmbad" "$s" --npm "${lib[@]}"
|
|
1676
|
+
# V13/V14: cannot-run cases exit 2.
|
|
1677
|
+
expect_status 2 'V13 no edge in that ecosystem' fxv verify-assets "$root/cons" "$s" --npm --consumer Mid --producer Lib
|
|
1678
|
+
mkdir -p "$root/noassets"
|
|
1679
|
+
expect_status 2 'V13 no project.assets.json' fxv verify-assets "$root/noassets" "$s" "${lib[@]}"
|
|
1680
|
+
expect_status 2 'V14 verify-pack --consumer without --producer' fxv verify-pack "$s" --consumer App
|
|
1681
|
+
expect_status 2 'V14 verify-pack --producer without --consumer' fxv verify-pack "$s" --producer Lib
|
|
1682
|
+
expect_status 2 'V14 verify-assets --consumer without --producer' fxv verify-assets "$root/cons" "$s" --consumer App
|
|
1683
|
+
expect_status 2 'V14 verify-assets --producer without --consumer' fxv verify-assets "$root/cons" "$s" --producer Lib
|
|
1684
|
+
# Manual forms stay available.
|
|
1685
|
+
expect_status 0 'name-mode verify-pack' fxv verify-pack "$s" Lib --ecosystem nuget
|
|
1686
|
+
expect_status 2 'name-mode verify-pack without ids' fxv verify-pack "$s" Nod
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
# Fixture matrix for the plan, closure and verify commands. Runs inside self_test and uses its
|
|
1690
|
+
# root/home/feed/st/passed/skipped (dynamic scope). `fx` is the fixture map, `real` the shipped one.
|
|
1691
|
+
# expect_status WANT DESCRIPTION COMMAND...: run, capture stdout/stderr in $root/output and $root/stderr, require the exit code.
|
|
1692
|
+
expect_status() {
|
|
1693
|
+
local want=$1 desc=$2 got=0
|
|
1694
|
+
shift 2
|
|
1695
|
+
"$@" > "$root/output" 2> "$root/stderr" || got=$?
|
|
1696
|
+
[ "$got" -eq "$want" ] || failed "self-test: $desc (exit $got, wanted $want)"
|
|
1697
|
+
passed=$((passed + 1))
|
|
1698
|
+
}
|
|
1699
|
+
tsv() { local IFS=$'\t'; printf '%s\n' "$*"; }
|
|
1700
|
+
self_test_matrix() {
|
|
1701
|
+
local fx=$root/fx.tsv status
|
|
1702
|
+
fxm() { env -i HOME="$home" PATH=/usr/bin:/bin:/usr/sbin:/sbin TMPDIR="$TMP" CRATIS_LOCAL_MAP="$fx" /bin/bash "$SELF" "$@"; }
|
|
1703
|
+
{
|
|
1704
|
+
echo '# fixture map'
|
|
1705
|
+
tsv pack Lib nuget Lib.slnx
|
|
1706
|
+
tsv pack Lib nuget Lib.Extra.slnx A=1,B=2
|
|
1707
|
+
tsv ids Lib nuget Cratis.Lib,Cratis.Lib.Core
|
|
1708
|
+
tsv pack Lib npm .
|
|
1709
|
+
tsv ids Lib npm @cratis/lib
|
|
1710
|
+
tsv pack Mid nuget Mid.slnx
|
|
1711
|
+
tsv ids Mid nuget Cratis.Mid
|
|
1712
|
+
tsv pack Extra nuget Extra.slnx
|
|
1713
|
+
tsv ids Extra nuget Cratis.Extra
|
|
1714
|
+
tsv pack Nod nuget Nod.slnx
|
|
1715
|
+
tsv uses Mid Lib nuget Directory.Packages.props
|
|
1716
|
+
tsv uses App Mid nuget Directory.Packages.props
|
|
1717
|
+
tsv uses App Lib nuget Directory.Packages.props
|
|
1718
|
+
tsv uses App Extra nuget Directory.Packages.props
|
|
1719
|
+
tsv uses App Lib npm .
|
|
1720
|
+
tsv uses App Orl nuget Directory.Packages.props
|
|
1721
|
+
tsv uses Orl Lib nuget Directory.Packages.props
|
|
1722
|
+
tsv uses App Nod nuget Directory.Packages.props
|
|
1723
|
+
} > "$fx"
|
|
1724
|
+
# P1: exact plan rows and the unnamed-hop warning on stderr only.
|
|
1725
|
+
{
|
|
1726
|
+
tsv pack Lib nuget; tsv pack Lib npm; tsv pack Mid nuget; tsv overlay Mid Lib
|
|
1727
|
+
tsv consume Lib nuget Directory.Packages.props; tsv consume Lib npm .; tsv consume Mid nuget Directory.Packages.props
|
|
1728
|
+
} > "$root/p1.expected"
|
|
1729
|
+
fxm plan App Lib Mid > "$root/output" 2> "$root/stderr" || failed 'self-test: P1 plan'
|
|
1730
|
+
cmp -s "$root/output" "$root/p1.expected" || failed 'self-test: P1 plan rows differ from the expected plan'
|
|
1731
|
+
has 'Lib -> Orl -> App' "$root/stderr" || failed 'self-test: P1 unnamed-hop warning'
|
|
1732
|
+
if has 'WARNING' "$root/output"; then failed 'self-test: P1 warning must stay on stderr'; fi
|
|
1733
|
+
# P2/P3: the real map. An NuGet-only edge plans only the NuGet ecosystem of a dual-ecosystem producer;
|
|
1734
|
+
# npm-only ids on a NuGet edge cannot be planned. P3 must move to the fixture map if Screenplay ships NuGet ids.
|
|
1735
|
+
st plan Stage Arc > "$root/output" 2> "$root/stderr" || failed 'self-test: P2 real plan'
|
|
1736
|
+
{ [ "$(grep -c '^pack' "$root/output")" -eq 1 ] && has "$(tsv pack Arc nuget)" "$root/output"; } || failed 'self-test: P2 plans exactly pack Arc nuget'
|
|
1737
|
+
status=0; st plan Stage Screenplay > "$root/output" 2> "$root/stderr" || status=$?
|
|
1738
|
+
{ [ "$status" -eq 2 ] && [ ! -s "$root/output" ] && has 'Screenplay (nuget)' "$root/stderr"; } || failed 'self-test: P3 npm-only ids on a NuGet edge'
|
|
1739
|
+
passed=$((passed + 3))
|
|
1740
|
+
# P4-P7: invalid inputs exit 2 with nothing on stdout (unknown, repeated, consumer as producer, wrong order, no edge, no ids, no producer).
|
|
1741
|
+
local bad
|
|
1742
|
+
for bad in 'App Nope' 'App Lib Lib' 'App Lib App' 'App Mid Lib' 'Mid Extra' 'App Nod' 'App'; do
|
|
1743
|
+
status=0
|
|
1744
|
+
# shellcheck disable=SC2086
|
|
1745
|
+
fxm plan $bad > "$root/output" 2> "$root/stderr" || status=$?
|
|
1746
|
+
{ [ "$status" -eq 2 ] && [ ! -s "$root/output" ]; } || failed "self-test: plan $bad must exit 2 with empty stdout (got $status)"
|
|
1747
|
+
passed=$((passed + 1))
|
|
1748
|
+
done
|
|
1749
|
+
# P8: a malformed row names its line.
|
|
1750
|
+
{ cat "$fx"; printf 'uses\tApp\tLib\n'; } > "$root/bad.tsv"
|
|
1751
|
+
status=0; fx=$root/bad.tsv; fxm plan App Lib > "$root/output" 2> "$root/stderr" || status=$?
|
|
1752
|
+
fx=$root/fx.tsv
|
|
1753
|
+
{ [ "$status" -eq 2 ] && has "bad.tsv:$(($(wc -l < "$root/bad.tsv"))): malformed" "$root/stderr"; } || failed 'self-test: P8 malformed map row'
|
|
1754
|
+
passed=$((passed + 1))
|
|
1755
|
+
# Pack properties are separate -p: arguments of the one command, never an environment variable.
|
|
1756
|
+
mkdir -p "$root/bin"; printf '#!/bin/sh\nexit 0\n' > "$root/bin/dotnet"; chmod +x "$root/bin/dotnet"
|
|
1757
|
+
env -i HOME="$home" PATH="$root/bin:/usr/bin:/bin" TMPDIR="$TMP" /bin/bash "$SELF" --dry-run pack-nuget "$repo" "$version" --property A=1 --property B=2 Lib.slnx > "$root/output" || failed 'self-test: pack-nuget --property dry run'
|
|
1758
|
+
{ has ' -p:A=1 -p:B=2 ' "$root/output" && ! has CRATIS_LOCAL_PACK_PROPERTIES "$root/output"; } || failed 'self-test: pack-nuget prints separate -p: tokens'
|
|
1759
|
+
status=0; env -i HOME="$home" PATH="$root/bin:/usr/bin:/bin" TMPDIR="$TMP" /bin/bash "$SELF" --dry-run pack-nuget "$repo" "$version" --property 'A=1;B' Lib.slnx > "$root/output" 2>&1 || status=$?
|
|
1760
|
+
[ "$status" -eq 2 ] || failed 'self-test: pack-nuget rejects an invalid property'
|
|
1761
|
+
status=0; env -i HOME="$home" PATH="$root/bin:/usr/bin:/bin" TMPDIR="$TMP" /bin/bash "$SELF" --dry-run pack-nuget "$repo" "$version" --property A=1 > "$root/output" 2>&1 || status=$?
|
|
1762
|
+
[ "$status" -eq 2 ] || failed 'self-test: pack-nuget needs a target after the properties'
|
|
1763
|
+
passed=$((passed + 3))
|
|
1764
|
+
self_test_closure
|
|
1765
|
+
self_test_verify
|
|
1766
|
+
}
|
|
1767
|
+
|
|
767
1768
|
case "$MODE" in
|
|
768
1769
|
stamp) stamp "$@" ;;
|
|
1770
|
+
plan) plan_command "$@" ;;
|
|
1771
|
+
closure) closure "$@" ;;
|
|
1772
|
+
verify-pack) verify_pack "$@" ;;
|
|
1773
|
+
verify-assets) verify_assets "$@" ;;
|
|
769
1774
|
pack-nuget) pack_nuget "$@" ;;
|
|
770
1775
|
pack-npm) pack_npm "$@" ;;
|
|
771
1776
|
use-nuget) use_nuget "$@" ;;
|