@smoothbricks/cli 0.11.10 → 0.11.12
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/dist/monorepo/cargo-policy.d.ts +25 -1
- package/dist/monorepo/cargo-policy.d.ts.map +1 -1
- package/dist/monorepo/cargo-policy.js +396 -4
- package/dist/monorepo/index.d.ts.map +1 -1
- package/dist/monorepo/index.js +5 -1
- package/dist/monorepo/packs/index.d.ts.map +1 -1
- package/dist/monorepo/packs/index.js +13 -0
- package/managed/raw/tooling/direnv/devenv.smoo.nix +6 -0
- package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +80 -21
- package/managed/templates/github/actions/setup-devenv/action.yml +33 -7
- package/package.json +2 -2
- package/src/monorepo/cargo-policy.test.ts +206 -3
- package/src/monorepo/cargo-policy.ts +341 -2
- package/src/monorepo/github-actions-bootstrap.test.ts +65 -11
- package/src/monorepo/index.ts +5 -1
- package/src/monorepo/packs/index.test.ts +32 -0
- package/src/monorepo/packs/index.ts +13 -0
|
@@ -31,20 +31,16 @@ add_repo_paths() {
|
|
|
31
31
|
# `--accept-flake-config` makes it worse than untidy: it pre-trusts the
|
|
32
32
|
# substituters declared by a commit nobody looked at.
|
|
33
33
|
#
|
|
34
|
-
# Resolved lazily, on the
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
if [ -n "${DEVENV_FLAKE:-}" ]; then
|
|
38
|
-
printf '%s' "$DEVENV_FLAKE"
|
|
39
|
-
return 0
|
|
40
|
-
fi
|
|
34
|
+
# Resolved lazily, on the paths that need it, because a host runner never does
|
|
35
|
+
# and that is the one branch which must not pay for a `nix eval`.
|
|
36
|
+
devenv_locked_rev() {
|
|
41
37
|
local lock="$repo_root/tooling/direnv/devenv.lock" rev
|
|
42
38
|
# nix, not jq: jq arrives with the shell this script is about to build, while
|
|
43
39
|
# nix is guaranteed by the install step before it. fromJSON also beats
|
|
44
40
|
# hand-rolled parsing of a file whose key order is not ours to assume.
|
|
45
41
|
# Absolute-path readFile needs --impure; nix's own error is left on stderr
|
|
46
|
-
# rather than swallowed, because an unresolvable pin is the failure
|
|
47
|
-
#
|
|
42
|
+
# rather than swallowed, because an unresolvable pin is the failure these
|
|
43
|
+
# functions exist to prevent.
|
|
48
44
|
rev="$(nix eval --raw --impure --expr \
|
|
49
45
|
"(builtins.fromJSON (builtins.readFile \"$lock\")).nodes.devenv.locked.rev")" || rev=""
|
|
50
46
|
if [ -z "$rev" ]; then
|
|
@@ -52,26 +48,89 @@ devenv_flake() {
|
|
|
52
48
|
echo " set DEVENV_FLAKE to install a devenv explicitly" >&2
|
|
53
49
|
return 1
|
|
54
50
|
fi
|
|
55
|
-
printf '
|
|
51
|
+
printf '%s' "$rev"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Whether a devenv binary is the commit the lock names. `devenv version` prints
|
|
55
|
+
# "devenv <semver>+<short rev> (<system>)", so the abbreviated commit is right
|
|
56
|
+
# there and a prefix test against the full rev is the entire comparison — no
|
|
57
|
+
# assumption about how many characters devenv chooses to abbreviate to. A build
|
|
58
|
+
# with no "+<rev>" at all cannot prefix a 40-char hex rev, so it reads as a
|
|
59
|
+
# mismatch, which is the safe direction.
|
|
60
|
+
devenv_matches_lock() {
|
|
61
|
+
local bin="$1" rev="$2" printed
|
|
62
|
+
printed="$("$bin" version 2>/dev/null)" || return 1
|
|
63
|
+
printed="${printed#*+}"
|
|
64
|
+
printed="${printed%% *}"
|
|
65
|
+
[ -n "$printed" ] || return 1
|
|
66
|
+
case "$rev" in
|
|
67
|
+
"$printed"*) return 0 ;;
|
|
68
|
+
esac
|
|
69
|
+
return 1
|
|
56
70
|
}
|
|
57
71
|
|
|
58
72
|
install_devenv() {
|
|
59
|
-
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
|
|
73
|
+
local rev flake found=""
|
|
74
|
+
# Host runners own their Nix profile, and that profile roots a /nix/store the
|
|
75
|
+
# whole fleet shares, so an image-provided devenv is authoritative here
|
|
76
|
+
# whatever commit it is — and a wrong one is never REPLACED, because
|
|
77
|
+
# `nix profile remove --all` on a host would take the fleet's other roots with
|
|
78
|
+
# it. SMOO_HOST_RUNNER comes from setup-devenv's runner-kind detection.
|
|
79
|
+
#
|
|
80
|
+
# With none present these runners install from the FLOATING branch, not the
|
|
81
|
+
# locked rev, which is the one place this script deliberately accepts drift.
|
|
82
|
+
# The pin exists to make the ephemeral runners' cached devenv reproducible;
|
|
83
|
+
# a host installs into a long-lived shared profile whose blast radius is the
|
|
84
|
+
# whole fleet, and pinning it down to an older CLI than the fleet had been
|
|
85
|
+
# running broke it: run 34374650577 segfaulted the Rust linker
|
|
86
|
+
# (devenv-rust-linker, clang exit 139) building cowshed-cli tests, where run
|
|
87
|
+
# 34371205556 on the same commit range was green with floating 2a399e9.
|
|
88
|
+
# 190959a is TWO COMMITS OLDER than what these hosts had been getting, so the
|
|
89
|
+
# pin was moving them backwards. A host's toolchain moves when its image or
|
|
90
|
+
# the lock moves, not when a repository decides to hold it back.
|
|
91
|
+
if [ "${SMOO_HOST_RUNNER:-false}" = true ]; then
|
|
92
|
+
if command -v devenv >/dev/null 2>&1; then
|
|
93
|
+
echo "using host devenv: $(command -v devenv) ($(devenv version))"
|
|
94
|
+
else
|
|
95
|
+
flake="${DEVENV_FLAKE:-github:cachix/devenv}"
|
|
96
|
+
echo "host runner has no devenv; nix profile add ${flake}"
|
|
97
|
+
nix profile add --accept-flake-config "$flake"
|
|
98
|
+
fi
|
|
99
|
+
devenv_path_and_caches
|
|
100
|
+
return 0
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
rev="$(devenv_locked_rev)"
|
|
104
|
+
# The store cache carries content only, never the Nix profiles, so the normal
|
|
105
|
+
# ephemeral case is that nothing is installed yet and this profile-adds the
|
|
106
|
+
# locked rev against an already-warm /nix — an evaluation and a link, not a
|
|
107
|
+
# download. Anything that does turn up on PATH is still checked rather than
|
|
108
|
+
# trusted, because "a devenv exists" was never the question.
|
|
65
109
|
if command -v devenv >/dev/null 2>&1; then
|
|
66
|
-
|
|
67
|
-
elif [ -x "$
|
|
68
|
-
|
|
110
|
+
found="$(command -v devenv)"
|
|
111
|
+
elif [ -x "$HOME/.nix-profile/bin/devenv" ]; then
|
|
112
|
+
found="$HOME/.nix-profile/bin/devenv"
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
if [ -n "$found" ] && devenv_matches_lock "$found" "$rev"; then
|
|
116
|
+
echo "using locked devenv: $found ($("$found" version))"
|
|
69
117
|
else
|
|
70
|
-
|
|
71
|
-
|
|
118
|
+
flake="${DEVENV_FLAKE:-github:cachix/devenv/$rev}"
|
|
119
|
+
if [ -n "$found" ]; then
|
|
120
|
+
# Replacing needs the old entry gone first: `nix profile add` would
|
|
121
|
+
# otherwise refuse on a bin/devenv collision at equal priority. --all is
|
|
122
|
+
# exact rather than blunt, because devenv is the only thing this script
|
|
123
|
+
# ever profile-adds, so a wrong devenv means a wrong profile.
|
|
124
|
+
echo "replacing devenv $("$found" version 2>/dev/null) — lock names ${rev:0:7}"
|
|
125
|
+
nix profile remove --all
|
|
126
|
+
fi
|
|
72
127
|
echo "nix profile add ${flake}"
|
|
73
128
|
nix profile add --accept-flake-config "$flake"
|
|
74
129
|
fi
|
|
130
|
+
devenv_path_and_caches
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
devenv_path_and_caches() {
|
|
75
134
|
if [ -d "$HOME/.nix-profile/bin" ]; then
|
|
76
135
|
echo "$HOME/.nix-profile/bin" >> "${GITHUB_PATH:-/dev/null}"
|
|
77
136
|
fi
|
|
@@ -119,9 +119,29 @@ runs:
|
|
|
119
119
|
keep-env-derivations = true
|
|
120
120
|
|
|
121
121
|
# The store itself, restored as files: /nix/store plus the Nix database,
|
|
122
|
-
# merged into the fresh install.
|
|
123
|
-
#
|
|
124
|
-
#
|
|
122
|
+
# merged into the fresh install. The post phase collects garbage down to the
|
|
123
|
+
# live closure and saves when the primary key missed.
|
|
124
|
+
#
|
|
125
|
+
# CONTENT ONLY — never the Nix profiles. `~/.nix-profile` and
|
|
126
|
+
# ~/.local/state/nix/profiles are mutable places whose whole job is to be
|
|
127
|
+
# gcroots, and restoring a previous run's copies over a freshly installed
|
|
128
|
+
# single-user Nix de-roots the Nix binary that is currently running. The post
|
|
129
|
+
# phase then garbage-collects it and cache-nix-action's own `nix --version`
|
|
130
|
+
# probe fails, at which point it skips the upload and still reports the step
|
|
131
|
+
# as a success — so the store cache silently stops being written and every
|
|
132
|
+
# later run pays a cold shell. Measured exactly that way: run 34371205556
|
|
133
|
+
# logged `nix (Nix) 2.34.7`, freed 2.7 GiB, then `nix: command not found`,
|
|
134
|
+
# and no `Cache saved` line for this key ever appeared.
|
|
135
|
+
#
|
|
136
|
+
# It only surfaced now because it needs a restore-prefix fallback rather than
|
|
137
|
+
# a cold store or an exact hit: a matching key restores profiles consistent
|
|
138
|
+
# with the store, while the diet rotated the key and pulled the previous
|
|
139
|
+
# store's profiles onto a different Nix.
|
|
140
|
+
#
|
|
141
|
+
# Dropping them costs nothing and buys determinism. devenv's closure still
|
|
142
|
+
# arrives inside /nix, so install-devenv's `nix profile add` of the locked
|
|
143
|
+
# rev is a link rather than a download, and the devenv in play is the one
|
|
144
|
+
# devenv.lock names instead of whichever run last populated the prefix.
|
|
125
145
|
- name: 🧊 Cache Nix store
|
|
126
146
|
id: nix-cache
|
|
127
147
|
if: steps.runner-kind.outputs.host != 'true'
|
|
@@ -129,12 +149,10 @@ runs:
|
|
|
129
149
|
with:
|
|
130
150
|
paths: |
|
|
131
151
|
/nix
|
|
132
|
-
~/.nix-profile
|
|
133
|
-
~/.local/state/nix/profiles
|
|
134
152
|
~/.cache/nix
|
|
135
153
|
# prettier-ignore
|
|
136
|
-
primary-key: ${{ runner.os }}-${{ runner.arch }}-nix-
|
|
137
|
-
restore-prefixes-first-match: ${{ runner.os }}-${{ runner.arch }}-nix-
|
|
154
|
+
primary-key: ${{ runner.os }}-${{ runner.arch }}-nix-v5-${{ hashFiles('tooling/direnv/devenv.yaml', 'tooling/direnv/devenv.nix', 'tooling/direnv/devenv.lock') }}
|
|
155
|
+
restore-prefixes-first-match: ${{ runner.os }}-${{ runner.arch }}-nix-v5-
|
|
138
156
|
gc-max-store-size: 1G
|
|
139
157
|
|
|
140
158
|
- name: ⚡ Enable Cachix
|
|
@@ -143,9 +161,17 @@ runs:
|
|
|
143
161
|
with:
|
|
144
162
|
name: devenv
|
|
145
163
|
|
|
164
|
+
# A host runner's devenv belongs to its image, so install-devenv accepts
|
|
165
|
+
# whatever is on PATH there. On an ephemeral runner the devenv came out of a
|
|
166
|
+
# cache this workflow wrote, so it is held to devenv.lock's rev: the store
|
|
167
|
+
# cache restores ~/.nix-profile wholesale and a prefix match hands the job
|
|
168
|
+
# the CLI of whichever run last populated the key, which outlived a lock bump
|
|
169
|
+
# until the check existed.
|
|
146
170
|
- name: 🧰 Install devenv
|
|
147
171
|
shell: bash
|
|
148
172
|
working-directory: tooling/direnv
|
|
173
|
+
env:
|
|
174
|
+
SMOO_HOST_RUNNER: ${{ steps.runner-kind.outputs.host }}
|
|
149
175
|
run: ./github-actions-bootstrap.sh install-devenv
|
|
150
176
|
|
|
151
177
|
# node_modules + ttsc plugin binaries: GH Actions cache on ephemeral runners.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smoothbricks/cli",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SmoothBricks monorepo automation CLI",
|
|
6
6
|
"bin": {
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
],
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@arethetypeswrong/core": "^0.18.2",
|
|
67
|
-
"@smoothbricks/nx-plugin": "0.4.
|
|
67
|
+
"@smoothbricks/nx-plugin": "0.4.6",
|
|
68
68
|
"@smoothbricks/validation": "0.1.8",
|
|
69
69
|
"commander": "^14.0.3",
|
|
70
70
|
"make-synchronized": "^0.8.0",
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it, spyOn } from 'bun:test';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
2
3
|
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
4
|
import { tmpdir } from 'node:os';
|
|
4
5
|
import { dirname, join } from 'node:path';
|
|
5
|
-
import { validateCargoCachePolicy } from './cargo-policy.js';
|
|
6
|
+
import { applyCargoFeatureUnification, type CargoHakariShell, validateCargoCachePolicy } from './cargo-policy.js';
|
|
6
7
|
|
|
7
8
|
async function withFixture<T>(files: Record<string, string>, callback: (root: string) => T): Promise<T> {
|
|
8
9
|
const root = await mkdtemp(join(tmpdir(), 'smoo-cargo-policy-'));
|
|
@@ -18,17 +19,46 @@ async function withFixture<T>(files: Record<string, string>, callback: (root: st
|
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
async function check(
|
|
22
|
+
async function check(
|
|
23
|
+
files: Record<string, string>,
|
|
24
|
+
shell?: CargoHakariShell,
|
|
25
|
+
): Promise<{ failures: number; messages: string[] }> {
|
|
22
26
|
return withFixture(files, (root) => {
|
|
23
27
|
const captured = captureErrors();
|
|
24
28
|
try {
|
|
25
|
-
return { failures: validateCargoCachePolicy(root), messages: captured.messages };
|
|
29
|
+
return { failures: validateCargoCachePolicy(root, { shell }), messages: captured.messages };
|
|
26
30
|
} finally {
|
|
27
31
|
captured.restore();
|
|
28
32
|
}
|
|
29
33
|
});
|
|
30
34
|
}
|
|
31
35
|
|
|
36
|
+
/** Records what update would run, and answers `verify` however the test needs. */
|
|
37
|
+
function recordingHakari(
|
|
38
|
+
verify: { code: number; output?: string; missing?: boolean } = { code: 0 },
|
|
39
|
+
): CargoHakariShell & {
|
|
40
|
+
calls: string[][];
|
|
41
|
+
} {
|
|
42
|
+
const calls: string[][] = [];
|
|
43
|
+
return {
|
|
44
|
+
calls,
|
|
45
|
+
run(_directory, args) {
|
|
46
|
+
calls.push([...args]);
|
|
47
|
+
return args[0] === 'verify'
|
|
48
|
+
? { code: verify.code, output: verify.output ?? '', missing: verify.missing ?? false }
|
|
49
|
+
: { code: 0, output: '', missing: false };
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const NIGHTLY_DEVENV = 'languages.rust = {\n channel = "nightly";\n};\n';
|
|
55
|
+
const UNIFIED_CONFIG = '[unstable]\nfeature-unification = true\n\n[resolver]\nfeature-unification = "workspace"\n';
|
|
56
|
+
const TWO_CRATE_WORKSPACE = {
|
|
57
|
+
'Cargo.toml': '[workspace]\nmembers = ["crates/*"]\n\n[profile.test]\nincremental = false\ndebug = 0\n',
|
|
58
|
+
'crates/alpha/Cargo.toml': '[package]\nname = "alpha"\n',
|
|
59
|
+
'crates/beta/Cargo.toml': '[package]\nname = "beta"\n',
|
|
60
|
+
};
|
|
61
|
+
|
|
32
62
|
function captureErrors(): { messages: string[]; restore: () => void } {
|
|
33
63
|
const messages: string[] = [];
|
|
34
64
|
const error = spyOn(console, 'error').mockImplementation((...args: unknown[]) => {
|
|
@@ -186,3 +216,176 @@ describe('Cargo cache policy', () => {
|
|
|
186
216
|
expect(result.messages).toEqual([]);
|
|
187
217
|
});
|
|
188
218
|
});
|
|
219
|
+
|
|
220
|
+
describe('Cargo workspace feature unification', () => {
|
|
221
|
+
it('leaves a single-crate workspace alone', async () => {
|
|
222
|
+
// Nothing to unify: `feature-unification = "workspace"` and a workspace-hack
|
|
223
|
+
// both exist to stop ONE dependency being built twice with different
|
|
224
|
+
// features for two members, which needs two members.
|
|
225
|
+
const result = await check({
|
|
226
|
+
'Cargo.toml': '[workspace]\nmembers = ["crates/only"]\n\n[profile.test]\nincremental = false\ndebug = 0\n',
|
|
227
|
+
'crates/only/Cargo.toml': '[package]\nname = "only"\n',
|
|
228
|
+
'tooling/direnv/devenv.smoo.nix': NIGHTLY_DEVENV,
|
|
229
|
+
});
|
|
230
|
+
expect(result.failures).toBe(0);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('requires a mechanism once a workspace holds more than one crate', async () => {
|
|
234
|
+
const result = await check({ ...TWO_CRATE_WORKSPACE, 'tooling/direnv/devenv.smoo.nix': NIGHTLY_DEVENV });
|
|
235
|
+
expect(result.failures).toBe(1);
|
|
236
|
+
expect(result.messages[0]).toContain('feature-unification');
|
|
237
|
+
expect(result.messages[0]).toContain('smoo monorepo update');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('accepts the nightly resolver configuration and rejects it without the unstable flag', async () => {
|
|
241
|
+
const configured = await check({
|
|
242
|
+
...TWO_CRATE_WORKSPACE,
|
|
243
|
+
'tooling/direnv/devenv.smoo.nix': NIGHTLY_DEVENV,
|
|
244
|
+
'.cargo/config.toml': UNIFIED_CONFIG,
|
|
245
|
+
});
|
|
246
|
+
expect(configured.failures).toBe(0);
|
|
247
|
+
|
|
248
|
+
// Cargo silently ignores [resolver] feature-unification without the
|
|
249
|
+
// unstable opt-in, so the half-configured repository believes it is unified
|
|
250
|
+
// while every crate still re-resolves features.
|
|
251
|
+
const inert = await check({
|
|
252
|
+
...TWO_CRATE_WORKSPACE,
|
|
253
|
+
'tooling/direnv/devenv.smoo.nix': NIGHTLY_DEVENV,
|
|
254
|
+
'.cargo/config.toml': '[resolver]\nfeature-unification = "workspace"\n',
|
|
255
|
+
});
|
|
256
|
+
expect(inert.failures).toBe(1);
|
|
257
|
+
expect(inert.messages[0]).toContain('[unstable] feature-unification = true');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('reads the channel from the managed devenv module, not from a decorative rust-toolchain file', async () => {
|
|
261
|
+
// devenv resolves the toolchain through rust-overlay and ignores
|
|
262
|
+
// rust-toolchain.toml unless languages.rust.toolchainFile names it, so the
|
|
263
|
+
// module's channel is the compiler that actually runs these commands.
|
|
264
|
+
const result = await check({
|
|
265
|
+
...TWO_CRATE_WORKSPACE,
|
|
266
|
+
'tooling/direnv/devenv.smoo.nix': NIGHTLY_DEVENV,
|
|
267
|
+
'rust-toolchain.toml': '[toolchain]\nchannel = "stable"\n',
|
|
268
|
+
'.cargo/config.toml': UNIFIED_CONFIG,
|
|
269
|
+
});
|
|
270
|
+
expect(result.failures).toBe(0);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('refuses the nightly-only resolver key on a stable toolchain', async () => {
|
|
274
|
+
const result = await check({
|
|
275
|
+
...TWO_CRATE_WORKSPACE,
|
|
276
|
+
'rust-toolchain.toml': '[toolchain]\nchannel = "stable"\n',
|
|
277
|
+
'.cargo/config.toml': UNIFIED_CONFIG,
|
|
278
|
+
});
|
|
279
|
+
expect(result.failures).toBe(1);
|
|
280
|
+
expect(result.messages[0]).toContain('cargo-hakari');
|
|
281
|
+
expect(result.messages[0]).toContain('stable');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('accepts a hakari-managed workspace-hack on a stable toolchain', async () => {
|
|
285
|
+
const hakari = recordingHakari();
|
|
286
|
+
const result = await check(
|
|
287
|
+
{
|
|
288
|
+
'Cargo.toml':
|
|
289
|
+
'[workspace]\nmembers = ["crates/*", "workspace-hack"]\n\n[profile.test]\nincremental = false\ndebug = 0\n',
|
|
290
|
+
'crates/alpha/Cargo.toml':
|
|
291
|
+
'[package]\nname = "alpha"\n\n[dependencies]\nworkspace-hack = { path = "../../workspace-hack" }\n',
|
|
292
|
+
'crates/beta/Cargo.toml':
|
|
293
|
+
'[package]\nname = "beta"\n\n[dependencies]\nworkspace-hack = { path = "../../workspace-hack" }\n',
|
|
294
|
+
'workspace-hack/Cargo.toml': '[package]\nname = "workspace-hack"\n',
|
|
295
|
+
'.config/hakari.toml': 'hakari-package = "workspace-hack"\nresolver = "2"\n',
|
|
296
|
+
'rust-toolchain.toml': '[toolchain]\nchannel = "stable"\n',
|
|
297
|
+
},
|
|
298
|
+
hakari,
|
|
299
|
+
);
|
|
300
|
+
expect(result.failures).toBe(0);
|
|
301
|
+
expect(hakari.calls).toEqual([['verify']]);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('flags a crate that does not depend on the workspace-hack', async () => {
|
|
305
|
+
const result = await check(
|
|
306
|
+
{
|
|
307
|
+
'Cargo.toml':
|
|
308
|
+
'[workspace]\nmembers = ["crates/*", "workspace-hack"]\n\n[profile.test]\nincremental = false\ndebug = 0\n',
|
|
309
|
+
'crates/alpha/Cargo.toml':
|
|
310
|
+
'[package]\nname = "alpha"\n\n[dependencies]\nworkspace-hack = { path = "../../workspace-hack" }\n',
|
|
311
|
+
'crates/beta/Cargo.toml': '[package]\nname = "beta"\n',
|
|
312
|
+
'workspace-hack/Cargo.toml': '[package]\nname = "workspace-hack"\n',
|
|
313
|
+
'.config/hakari.toml': 'hakari-package = "workspace-hack"\n',
|
|
314
|
+
'rust-toolchain.toml': '[toolchain]\nchannel = "stable"\n',
|
|
315
|
+
},
|
|
316
|
+
recordingHakari(),
|
|
317
|
+
);
|
|
318
|
+
expect(result.failures).toBe(1);
|
|
319
|
+
expect(result.messages[0]).toContain('beta');
|
|
320
|
+
expect(result.messages[0]).toContain('smoo monorepo update');
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('surfaces a stale workspace-hack that cargo hakari verify rejects', async () => {
|
|
324
|
+
const result = await check(
|
|
325
|
+
{
|
|
326
|
+
'Cargo.toml':
|
|
327
|
+
'[workspace]\nmembers = ["crates/*", "workspace-hack"]\n\n[profile.test]\nincremental = false\ndebug = 0\n',
|
|
328
|
+
'crates/alpha/Cargo.toml':
|
|
329
|
+
'[package]\nname = "alpha"\n\n[dependencies]\nworkspace-hack = { path = "../../workspace-hack" }\n',
|
|
330
|
+
'crates/beta/Cargo.toml':
|
|
331
|
+
'[package]\nname = "beta"\n\n[dependencies]\nworkspace-hack = { path = "../../workspace-hack" }\n',
|
|
332
|
+
'workspace-hack/Cargo.toml': '[package]\nname = "workspace-hack"\n',
|
|
333
|
+
'.config/hakari.toml': 'hakari-package = "workspace-hack"\n',
|
|
334
|
+
'rust-toolchain.toml': '[toolchain]\nchannel = "stable"\n',
|
|
335
|
+
},
|
|
336
|
+
recordingHakari({ code: 1, output: 'workspace-hack is not up-to-date' }),
|
|
337
|
+
);
|
|
338
|
+
expect(result.failures).toBe(1);
|
|
339
|
+
expect(result.messages[0]).toContain('cargo hakari verify');
|
|
340
|
+
expect(result.messages[0]).toContain('workspace-hack is not up-to-date');
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe('Cargo feature unification update', () => {
|
|
345
|
+
it('writes the nightly resolver configuration once', async () => {
|
|
346
|
+
await withFixture({ ...TWO_CRATE_WORKSPACE, 'tooling/direnv/devenv.smoo.nix': NIGHTLY_DEVENV }, (root) => {
|
|
347
|
+
const configPath = join(root, '.cargo/config.toml');
|
|
348
|
+
const hakari = recordingHakari();
|
|
349
|
+
applyCargoFeatureUnification(root, { shell: hakari });
|
|
350
|
+
const written = readFileSync(configPath, 'utf8');
|
|
351
|
+
expect(written).toContain('[unstable]\nfeature-unification = true');
|
|
352
|
+
expect(written).toContain('[resolver]\nfeature-unification = "workspace"');
|
|
353
|
+
expect(hakari.calls).toEqual([]);
|
|
354
|
+
|
|
355
|
+
applyCargoFeatureUnification(root, { shell: hakari });
|
|
356
|
+
expect(readFileSync(configPath, 'utf8')).toBe(written);
|
|
357
|
+
const captured = captureErrors();
|
|
358
|
+
try {
|
|
359
|
+
expect(validateCargoCachePolicy(root, { shell: hakari })).toBe(0);
|
|
360
|
+
} finally {
|
|
361
|
+
captured.restore();
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('generates and wires a workspace-hack on a stable toolchain', async () => {
|
|
367
|
+
await withFixture(
|
|
368
|
+
{ ...TWO_CRATE_WORKSPACE, 'rust-toolchain.toml': '[toolchain]\nchannel = "1.89.0"\n' },
|
|
369
|
+
(root) => {
|
|
370
|
+
const hakari = recordingHakari();
|
|
371
|
+
applyCargoFeatureUnification(root, { shell: hakari });
|
|
372
|
+
expect(hakari.calls).toEqual([['init', 'workspace-hack'], ['generate'], ['manage-deps', '--yes']]);
|
|
373
|
+
},
|
|
374
|
+
);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('skips hakari init when the workspace already declares one', async () => {
|
|
378
|
+
await withFixture(
|
|
379
|
+
{
|
|
380
|
+
...TWO_CRATE_WORKSPACE,
|
|
381
|
+
'rust-toolchain.toml': '[toolchain]\nchannel = "1.89.0"\n',
|
|
382
|
+
'.config/hakari.toml': 'hakari-package = "workspace-hack"\n',
|
|
383
|
+
},
|
|
384
|
+
(root) => {
|
|
385
|
+
const hakari = recordingHakari();
|
|
386
|
+
applyCargoFeatureUnification(root, { shell: hakari });
|
|
387
|
+
expect(hakari.calls).toEqual([['generate'], ['manage-deps', '--yes']]);
|
|
388
|
+
},
|
|
389
|
+
);
|
|
390
|
+
});
|
|
391
|
+
});
|